Ubisam-MobileGit/tasks/023-tolerate-null-list-responses.md
choiminyeong 7da04a77ae Add TASK-023: tolerate a literal JSON null list response
Gitea's Go handlers serialize a nil slice as the JSON literal `null`
rather than `[]` for some list endpoints when there are no items
(e.g. a repository with no branches). parseList treated that as a
parse failure and threw ApiFailure.InvalidResponse, surfacing as a
misleading "server error" on a completely successful 200 OK.

Diagnosed live against the human product owner's real Gitea instance
via wireless adb (repositories CSOT-T8-iPTN-AMA / CSOT-T8-MACRO_SA):
confirmed via the Gitea container's own request log that the call
was a genuine 200 OK, then used temporary debug-only logging to
narrow the cause to the literal null body. Fixed at the shared
parseList helper so every sibling list endpoint benefits, not just
the one that was reported.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 23:04:43 +09:00

49 lines
4.3 KiB
Markdown

# TASK-023: Tolerate a literal JSON null list response
Status: implemented; all required checks pass; physical-device retest confirmed
Owner: Claude
Reviewer: Codex
## Outcome
A repository whose branch (or any other list-shaped) endpoint returns the literal JSON body `null` — Go's `encoding/json` serializes a `nil` slice this way instead of `[]` — is treated as an empty list, not a parse failure. Previously this surfaced as a generic "Gitea 서버에서 오류가 발생했습니다" (server error) on both the repository-list row's last-pushed-branch indicator and the repository's reference (branch/tag) screen, even though the HTTP response itself was a completely successful `200 OK`.
## Incident evidence
Reproduced live on the human product owner's authenticated device via wireless `adb`, against real repositories `RobotAutomationDevelopment-Dept/CSOT-T8-iPTN-AMA` and `CSOT-T8-MACRO_SA`. Confirmed via the Gitea container's own request log (Container Manager → 로그) that the branches request returned `200 OK` in ~7ms — not a server-side failure. A temporary debug-only diagnostic (`Log.e`, removed before commit) then captured the actual response body for the failing call: `bodyLen=5 bodyPreview=null` — the literal 4-character JSON `null` (plus a trailing artifact in the length count), not an array. `GiteaRepositoryBrowserApi.parseList` deserialized that to Kotlin `null` via Gson (no exception), then unconditionally threw `ApiFailure.InvalidResponse()` on a null parse result — a general-purpose fallback meant for a body that failed to deserialize at all, not for a body that correctly deserializes to "no items."
## Acceptance criteria
- [x] A JSON `null` response body from any `parseList`-backed endpoint (branches, tags, commits, etc.) maps to an empty list rather than a thrown failure.
- [x] Existing malformed/genuinely-invalid JSON still throws `ApiFailure.InvalidResponse()` (only the specific null-body case changed).
- [x] A unit test reproduces the literal `null` body case for `listBranches`.
- [x] Verified live on the reporting device: `CSOT-T8-iPTN-AMA` (the repository used for diagnosis) now opens correctly instead of showing the generic server-error screen. `CSOT-T8-MACRO_SA` was not individually re-confirmed by the human product owner, but is expected to behave identically since the fix is in the shared `parseList` helper both repositories go through.
- [x] Build, unit tests, lint, and secret scan pass.
## Writable paths
- `app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt`
- `app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt`
- `tasks/023-tolerate-null-list-responses.md`
## Required checks
```text
./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug
```
## Evidence
- `GiteaRepositoryBrowserApi.kt`'s private `parseList<T>` helper: `gson.fromJson<List<T>>(body, type) ?: throw ApiFailure.InvalidResponse()` changed to `?: emptyList()`. This is the single shared helper used by `listBranches`, `listTags`, `listCommits`, and repository listing, so the fix applies uniformly rather than only to the one endpoint that happened to be reported — the same Go nil-slice-to-null serialization can equally affect any of them.
- A genuinely malformed body (unparseable JSON) still throws `ApiFailure.InvalidResponse()` via the existing `catch (_: JsonParseException)` — only a body that Gson *successfully* parses to `null` changed behavior.
- Diagnostic method: added temporary `BuildConfig.DEBUG`-gated `Log.e` calls at three points (`loadReferences`'s catch, `listBranches`'s per-item mapping, and the raw response body) to narrow the failure down via live logcat over a wireless-`adb` connection to the reporting device, then removed all three before committing — none of that scaffolding is part of this change.
- `./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks`: BUILD SUCCESSFUL, all unit tests passing, 0 lint errors.
- `git diff --check`: clean. Secret-pattern scan of the diff: no matches.
- Live device retest: reinstalled the fixed build over wireless `adb` on the reporting device (paired via wireless debugging in this session) and the human product owner confirmed `CSOT-T8-iPTN-AMA` now opens correctly.
## Review findings
None yet — pending Codex/human review.