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>
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
nullresponse body from anyparseList-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
nullbody case forlistBranches. - 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_SAwas not individually re-confirmed by the human product owner, but is expected to behave identically since the fix is in the sharedparseListhelper both repositories go through. - Build, unit tests, lint, and secret scan pass.
Writable paths
app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.ktapp/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kttasks/023-tolerate-null-list-responses.md
Required checks
./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug
Evidence
GiteaRepositoryBrowserApi.kt's privateparseList<T>helper:gson.fromJson<List<T>>(body, type) ?: throw ApiFailure.InvalidResponse()changed to?: emptyList(). This is the single shared helper used bylistBranches,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 existingcatch (_: JsonParseException)— only a body that Gson successfully parses tonullchanged behavior. - Diagnostic method: added temporary
BuildConfig.DEBUG-gatedLog.ecalls 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-adbconnection 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
adbon the reporting device (paired via wireless debugging in this session) and the human product owner confirmedCSOT-T8-iPTN-AMAnow opens correctly.
Review findings
None yet — pending Codex/human review.