Skip to content

fix: handle missing and null fields in API responses - #270

Merged
mkb79 merged 1 commit into
masterfrom
fix/null-field-handling
Jul 30, 2026
Merged

fix: handle missing and null fields in API responses#270
mkb79 merged 1 commit into
masterfrom
fix/null-field-handling

Conversation

@mkb79

@mkb79 mkb79 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes #268.

Problem

Five crashes sharing one root pattern: the code assumes a field is present and non-null in an API response, and raises an unhelpful low-level exception when it is not.

Where Trigger Was
filter_by_date() purchase_date: null + missing/null library_status, with any date bound AttributeError: 'NoneType' object has no attribute 'get'
_reuse_voucher() refresh_date: null TypeError: strptime() argument 1 must be str, not None
get_license() license_denial_reasons: null on a denied license TypeError: 'NoneType' object is not iterable instead of LicenseDenied
get_license() content_metadata: null or content_url: null AttributeError instead of NoDownloadUrl
is_published() missing publication_datetime falls through to None, callers pass it to ItemNotPublished, which fails in the parser

The first three rows all come from testing key presence where the value needed testing. Case 1 is reachable in practice: podcast episodes fetched through the catalog endpoint carry neither purchase_date nor library_status (#267).

Behaviour change

A missing publication_datetime now means unknown, not unpublished. Skipping an item on absent evidence silently drops content, so is_published() returns True and lets the download attempt itself decide. An AudioPart whose parent carries no publication date falls back to its own date, so a known future date is still honoured — only when neither is available is the item assumed published.

ItemNotPublished no longer raises while building its message; without a usable date it reports the ASIN without a countdown.

Verification

  • library_status missing / null / date_added null — all three keep the item; the date_added branch still filters correctly
  • refresh_date null and absent are skipped; expired → VoucherNeedRefresh, valid → accepted, expired ExpiresDownloadUrlExpired all unchanged
  • license_denial_reasons null → LicenseDenied; content_metadata/content_url null → NoDownloadUrl; a denied license with real reasons is unchanged
  • is_published() across the parent/child matrix — parent date stays authoritative, fallback only when the parent has none, both absent → True; non-AudioPart unchanged
  • ItemNotPublished with None, "", "garbage" and an overflowing offset → clean message; a real date still yields the countdown
  • The timezone-aware suite from refactor!: make all API and download datetimes timezone-aware UTC #266 still passes
  • ruff check src plugin_cmds: +1 G004 for the new logger.debug f-string, matching the six already in that file
  • Real API: audible library list --resolve-podcasts --start-date 2008-01-01 against an account with 168 titles returns 914 items, identical to master in the same window, exit 0, no stderr

Review notes

A review pass caught a regression in the first version of the is_published() change: returning True on a null parent date ignored a valid future date on the child. That is fixed and covered by the parent/child matrix above. The same pass found the two get_license() cases, which are the same defect class as the three in #268 and are therefore included here rather than deferred to another issue.

Two related items deliberately left out, both pre-existing:

  • Callers pass self.publication_datetime to ItemNotPublished even when is_published() evaluated the parent's date, so a future parent with a past child can produce a misleading countdown.
  • The bundled example plugins carry similar null assumptions (library_status["date_added"] in cmd_goodreads-transform.py, product_images.items() in cmd_image-urls.py). Not built-in command paths.

Five crashes that share one root pattern: the code assumed a field was
present and non-null, and raised an unhelpful low-level exception when the
API said otherwise.

`filter_by_date()` called `.get()` on `library_status` without checking it is
a dict, so an item with `purchase_date: null` and a missing or null
`library_status` raised `AttributeError` as soon as a date bound was set. The
`else` branch already handles "cannot determine date added" by logging and
keeping the item; it just could not be reached. This shape is not
hypothetical: podcast episodes fetched through the catalog endpoint carry
neither field (#267).

`_reuse_voucher()` tested for key presence and then converted the value, so
an explicit `null` passed the guard and reached `strptime` as `None`. Test
the value instead of the key.

`get_license()` did the same for `license_denial_reasons`, turning a denied
license into `TypeError: 'NoneType' object is not iterable` instead of
`LicenseDenied`. A null `content_metadata` or `content_url` likewise raised
`AttributeError` instead of reaching the intended `NoDownloadUrl`.

`is_published()` had no explicit return for a missing `publication_datetime`
and fell through to `None`. Callers read that as "not published" and passed
the same missing value into `ItemNotPublished`, which then failed in the
parser. A missing publication date means unknown, not unpublished, so
skipping the item on absent evidence silently drops it — return `True` and
let the download attempt itself decide. An `AudioPart` whose parent has no
publication date now falls back to its own instead of being assumed
published, so a known future date is still honoured.

`ItemNotPublished` additionally no longer raises while building its message:
without a usable date it reports the ASIN without a countdown.

Closes #268
@mkb79
mkb79 merged commit bd2e867 into master Jul 30, 2026
@mkb79
mkb79 deleted the fix/null-field-handling branch July 30, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: crashes on missing or null fields in API responses (library_status, refresh_date, publication_datetime)

1 participant