Ubisam-MobileGit/tasks/023-tolerate-null-list-responses.md
choiminyeong f6ac20c8ed TASK-063: task records say what is actually open
No task waits on a Codex review; the redesign tasks are confirmed on the
product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric
reauthentication.

Agent: main (opus)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:25:49 +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 — no Codex review pending (Codex retired, 2026-09-29).