[TV] Fix search failing on unknown combined-search result types - #5745
Conversation
|
Claude finished @sztomek's task in 2m 36s —— View job Code reviewThe diagnosis and the fix are both right. Two inline comments posted. Summary of everything I found: 1.
|
|
Claude finished @sztomek's task in 2m 20s —— View job Code review (re-run on ready for review)The fix is correct and I'd ship it. One correction to my earlier review, since it changes what you might do:
|
|
Claude finished @sztomek's task in 2m 1s —— View job Code review (re-run on push — "Address PR comments")The new commit picks up both of my earlier items, and the result is a tighter fix than the original. Verified against the current tree:
The core reasoning holds: Findings1. The new
|
| val title: String? = null, | ||
| val author: String? = "", | ||
| val slug: String, | ||
| val slug: String = "", |
There was a problem hiding this comment.
Good change — slug and podcastSlug are read by nobody (ImprovedSearchManagerImpl.kt:37-61 ignores both, and there are no other consumers of CombinedResult), so requiring them was pure downside.
One caveat on how far a Kotlin default actually protects you: Moshi's generated adapter uses the default only when the key is absent. If the server sends "slug": null explicitly, codegen still hits Util.unexpectedNull(...) and throws JsonDataException: Non-null value 'slug' was null at $.results[N], which aborts the whole response exactly like the old bug. String? = null covers both the absent and explicit-null cases; String = "" covers only the first. Given the motivation here is "one weird result shouldn't wipe out the search", nullable is the stronger guarantee.
Related, and the same argument as slug: url (line 29) is still non-null-required and is likewise never read by any consumer. title/podcastUuid/podcastTitle are at least consumed, though it's a bit asymmetric that PodcastResult.title is nullable-and-filtered (ImprovedSearchManagerImpl.kt:40) while EpisodeResult.title hard-fails the response.
Not blocking — the PR's stated bug is fixed either way.
|
Version |
|
Claude finished @sztomek's task in 1m 58s —— View job Code review (re-run on push —
|
| Earlier item | Status |
|---|---|
= "" defaults don't cover an explicit null |
✅ Fixed — slug, podcastSlug, and url are now String? = null (CombinedSearchResponse.kt:19,29,36) |
Regression test for explicit null |
✅ Added (CombinedSearchResponseTest.kt:48-62) |
AutoCompleteResult had the same latent bug |
✅ Fixed in the previous commit |
Both changes are safe: I grepped every reference to CombinedResult across the repo and slug / podcastSlug / url are read by no consumer — ImprovedSearchManagerImpl.kt:39-57 is the only mapping site and it touches none of them. So widening them to nullable can't break a call site, and it removes three more ways a single malformed result could take down the whole response.
Both when blocks stay exhaustive over their sealed types, so a future subtype is a compile error rather than a silent miss.
Findings
1. Fields nobody reads could just be deleted (low, simplification)
slug, podcastSlug, and url are now nullable-with-default and read by zero consumers. Moshi's generated adapters skipValue() unknown JSON keys by default, so removing the three properties outright is behaviourally identical to keeping them nullable — and it's one fewer thing that can go wrong when the server changes shape. Keeping them is defensible if you expect to surface episode url or the slugs soon; otherwise they're dead weight that still has to be parsed and allocated per result.
2. EpisodeResult.title / podcastUuid / podcastTitle are the last whole-response failure mode (low)
CombinedSearchResponse.kt:26,32,34 are still non-null-required, so one episode result missing (or nulling) podcast_title still throws JsonDataException and wipes the entire search — the exact symptom this PR exists to kill, just with a rarer trigger. Unlike the slugs, these are consumed, so the fix isn't free: making them nullable means either dropping the item in ImprovedSearchManagerImpl (mirroring what PodcastResult.title already does at line 40) or defaulting. Worth noting the current asymmetry — PodcastResult.title is nullable-and-filtered, EpisodeResult.title hard-fails the response. Follow-up material, not a blocker.
3. Dropped results are still invisible (low, unchanged)
Neither Unknown branch (ImprovedSearchManagerImpl.kt:30,59) logs a discard. If the server broadens network results or adds a type, the client silently returns a shorter list and nobody finds out. A LogBuffer.i would make the next occurrence visible. Separate product question: network results are an intentional server feature, so dropping them means podcast networks never surface in search — worth a tracking issue if one doesn't exist.
Smaller notes
- The new test's episode payload omits
duration, exercising the0Ldefault alongside the explicit nulls — nice coverage bonus. PodcastResult.author: String? = ""(line 18) is a nullable field with an empty-string default, which the consumer then.orEmpty()s anyway. Pre-existing and harmless, but the default buys nothing.- Pre-existing:
publishedDate: Date = Date()(line 28) substitutes "now" for a missing date, so a malformed episode renders with a wrong-but-plausible timestamp rather than being dropped. - No security or performance implications — no new I/O, allocation, or parsing on any hot path; the nullable widening removes potential exception throws.
Verification
I still could not run ./gradlew — the command requires approval this job doesn't have. Both test tasks from the PR description are unverified by me. Reading them, the assertions follow from the code: Moshi codegen assigns null directly for a nullable property whose JSON value is explicitly null (no unexpectedNull check is generated), and uses the Kotlin default only when the key is absent — so all three cases in CombinedSearchResponseTest hold. Add Bash(./gradlew*) to --allowedTools if you'd like future reviews to execute the suite.
Verdict: ships. Every item I raised across the three reviews is now addressed; the remaining points are follow-up-grade. No changes pushed — review feedback only.
- Gather context (PR diff vs
origin/main— note: the checkout is shallow with no merge base, so I reviewed commit-by-commit plus the changed-file list) - Read changed files + grep all consumers of
CombinedResult/AutoCompleteResult - Confirm the previous findings are addressed
- Check that widening
slug/url/podcastSlugto nullable breaks no call site - Attempt to run tests — blocked, see Verification
- Post review
• branchfix/tv-search-network-crash
Description
Search hard-fails whenever the
/search/combinedresponse contains a result the client doesn't recognise. The response is decoded with a MoshiPolymorphicJsonAdapterFactorykeyed ontype, registered only forpodcastandepisode. The server also returns"type":"network"results (podcast networks), and the factory throwsJsonDataException: Expected one of [podcast, episode] for key 'type' but found 'network'— which aborts decoding of the entire response, so a search that returned perfectly good podcasts and episodes surfaces as a generic error with zero results.This reproduces for common queries (e.g.
news,search) and affects both phone and TV search (both go throughImprovedSearchManager.combinedSearch). Verified live:POST /search/combinedreturns HTTP 200 with valid results, yet the app shows the error state, with this stacktrace:Fix
Add an
Unknownfallback to the polymorphic adapter (withDefaultValue(Unknown)) so any unrecognisedtype(todaynetwork, and any future type) decodes toCombinedResult.Unknowninstead of throwing.ImprovedSearchManagerImpl.combinedSearchalready usesmapNotNull, so unknown results are simply dropped and the rest of the results render normally.Testing Instructions
news) on phone or TV → results now appear instead of an error../gradlew :modules:services:servers:testDebugUnitTest --tests "*CombinedSearchResponseTest*" :modules:services:repositories:testDebugUnitTest --tests "*ImprovedSearchManagerImplTest*"— covers the adapter tolerating anetworkresult and the mapping droppingUnknown.Screenshots or Screencast
Screen_recording_20260814_175406.mp4
Screen_recording_20260814_175445.mp4
Checklist
./gradlew spotlessApply)