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>
This commit is contained in:
parent
b46dd3a72b
commit
7da04a77ae
@ -291,9 +291,12 @@ class GiteaRepositoryBrowserApi(
|
||||
throw ApiFailure.InvalidResponse()
|
||||
}
|
||||
|
||||
// Gitea's Go handlers serialize a nil slice as JSON `null` rather than `[]` for some
|
||||
// list endpoints when the underlying collection is empty (e.g. a repository with no
|
||||
// branches) — that is a valid "no items" response, not a malformed one.
|
||||
private inline fun <reified T> parseList(body: String): List<T> = try {
|
||||
val type = object : TypeToken<List<T>>() {}.type
|
||||
gson.fromJson<List<T>>(body, type) ?: throw ApiFailure.InvalidResponse()
|
||||
gson.fromJson<List<T>>(body, type) ?: emptyList()
|
||||
} catch (_: JsonParseException) {
|
||||
throw ApiFailure.InvalidResponse()
|
||||
}
|
||||
|
||||
@ -174,6 +174,18 @@ class GiteaRepositoryBrowserApiTest {
|
||||
assertEquals(listOf(null, null), branches.items.map { it.commitAtEpochMillis })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a literal JSON null body maps to an empty page instead of failing closed`() = runTest {
|
||||
// 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).
|
||||
val transport = QueueTransport(response("null"))
|
||||
|
||||
val branches = api(transport).listBranches(repository, 1, 20)
|
||||
|
||||
assertTrue(branches.items.isEmpty())
|
||||
assertNull(branches.nextPage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tree preserves ref and encoded path while returning a local immutable page`() = runTest {
|
||||
val transport = QueueTransport(
|
||||
|
||||
@ -333,3 +333,16 @@ This is the append-only implementation and handoff record shared by Codex and Cl
|
||||
- Known limitations: Not retested live against `CSOT-T8-iPTN-AMA` (or any repository with a real missing/malformed branch timestamp) on a physical device with this exact fix — Codex reproduced and diagnosed the original crash on-device, but this fix itself has only unit-test coverage so far.
|
||||
- Next recommended task: A physical-device retest of this fix (Codex has working wireless-adb device access per TASK-020/021's evidence). No other roadmap items are open.
|
||||
- Next owner: Codex or the human product owner, for the device retest; otherwise no further items are unassigned.
|
||||
|
||||
## 2026-08-26 — TASK-023: tolerate a literal JSON null list response
|
||||
|
||||
- Agent and role: Claude, live incident diagnosis and fix, with the human product owner's own device as the reporting/verification device (their first real end-to-end use of the app this session).
|
||||
- Commit/diff: continued on `claude/task-015-notifications`; `data/repositories/GiteaRepositoryBrowserApi.kt`, `data/repositories/GiteaRepositoryBrowserApiTest.kt`.
|
||||
- Features implemented: A `parseList`-backed endpoint (branches, tags, commits, repository listing) whose response body is the literal JSON `null` — Go's `encoding/json` serializes a `nil` slice this way instead of `[]` — now maps to an empty list instead of throwing `ApiFailure.InvalidResponse()`. Previously this showed a misleading "Gitea 서버에서 오류가 발생했습니다" (server error) message on a completely successful `200 OK` response.
|
||||
- How it was diagnosed: The human product owner set up wireless `adb` debugging to their own physical device this session (paired via `adb pair`/`adb connect`); this was used to install successive debug builds directly and read `logcat` live rather than round-tripping screenshots. First confirmed via the Gitea server's own Container Manager request log that the failing `branches` call was a genuine `200 OK` in ~7ms, ruling out an actual server-side 5xx despite the app's own error message claiming one. Then added three rounds of temporary `BuildConfig.DEBUG`-gated `Log.e` diagnostics (the caught exception, per-item mapping detail, then the raw response body) to narrow the cause down to `bodyPreview=null` — a literal `null` string, not empty or malformed JSON. All diagnostic logging was removed before committing; only the real fix and a matching test remain.
|
||||
- Files changed: `data/repositories/GiteaRepositoryBrowserApi.kt`, `data/repositories/GiteaRepositoryBrowserApiTest.kt`, `tasks/023-tolerate-null-list-responses.md`.
|
||||
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors; `git diff --check` clean; secret-pattern scan clean. Verified live: reinstalled over wireless `adb` and the human product owner confirmed `CSOT-T8-iPTN-AMA` now opens correctly.
|
||||
- Review findings and resolutions: The fix was made at the shared `parseList` helper level, not narrowly in `listBranches`, since the same Go nil-slice-to-null serialization can equally affect tags, commits, or repository listing — narrowing it to only the reported endpoint would have left the same latent bug in every sibling endpoint.
|
||||
- Known limitations: `CSOT-T8-MACRO_SA` (the other repository that showed the same symptom) was not individually re-confirmed by the human product owner, though it goes through the identical code path.
|
||||
- Next recommended task: None outstanding on the proposed roadmap. TASK-016's two previously-blocked items (full authenticated-screen device regression, R8/ProGuard minification decision) are now more tractable, since a real device session with working wireless `adb` and the human product owner's active participation is established for the first time this session.
|
||||
- Next owner: Claude, continuing per the standing direction, or the human product owner if they'd rather drive further device testing directly.
|
||||
|
||||
48
tasks/023-tolerate-null-list-responses.md
Normal file
48
tasks/023-tolerate-null-list-responses.md
Normal file
@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
Loading…
x
Reference in New Issue
Block a user