What's fixed
If your library has a calibre.db data row with a NULL name column (anomalous but it happens — old imports, partial inserts, certain migrations), book downloads for that row now return a clean 400 with a diagnostic log line naming the offending book id and format. Pre-fix: 500 with an unhelpful TypeError: 'NoneType' and 'str' deep inside do_download_file().
Same clean rejection if a caller passes a None book_format (defensive — Flask routing normally guarantees a string).
Resolves #103 and mirrors janeczku/calibre-web#3274 (the upstream user-reported 500).
docker pull ghcr.io/new-usemame/calibre-web-nextgen:v4.0.34
Why this isn't a try/except mask
The new guard is a precondition at the function entry. It surfaces which row is broken (book id + format in the log line) so the operator can fix the underlying calibre.db. If a data row has a NULL name, the right answer is to fix that row in calibre.db; the guard makes that diagnosis trivial. It doesn't paper over the issue.
Regression coverage
8 new unit tests pin all rejection paths (data.name None / empty, book_format None / empty / non-string, data itself None) and a valid-pass-through assertion. An AST source-pin asserts the guard appears before any string-concatenation operation in the function body, so a future refactor can't quietly drop it.