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

4.3 KiB

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

  • A JSON null response body from any parseList-backed endpoint (branches, tags, commits, etc.) maps to an empty list rather than a thrown failure.
  • Existing malformed/genuinely-invalid JSON still throws ApiFailure.InvalidResponse() (only the specific null-body case changed).
  • A unit test reproduces the literal null body case for listBranches.
  • 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.
  • 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

./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.