PR diff fails with "Cannot read properties of null (reading 'patch')" and sticks on "loading diff…" with Files (0) #520
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#520
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's requested
Opening a PR's Files tab (and in the same failure class, a commit page diff) can throw
TypeError: Cannot read properties of null (reading 'patch')and then sit onloading diff…forever with a header reading Files on #N (0). The page must handle a null/empty diff payload honestly: show the real file count (including a truthful "0 files changed"), and on fetch/parse failure render a human error state with an explicit Retry control — never an eternal spinner.Evidence (static analysis; not reproduced locally, per standing rule)
GET …/api/pulls/{num}/diffanswersContent-Type: text/plain; charset=utf-8(internal/pulls/http.gogetDiff, ~line 581), but the SDK callclient._call(p(\/pulls/${num}/diff`), { method: "GET" })(web/sdk/src/pulls.js~line 45) does **not** passraw: true._dispatch(web/sdk/src/core.js~line 484) ignores content-type for non-SSE and routes **every** non-SSE response through_jsonResponse, which JSON-parses the body: a real patch body would throwReposError: invalid JSON from …, and an **empty patch body** (git diff base...headreturns "" — e.g. an already-merged PR, or base == head) takes thetext === ""branch and resolves **null`**.res.patchon that null:web/src/pages/PullFiles.jsx~line 56:typeof res === "string" ? res : res.patch ?? res.diff ?? ""web/src/pages/Pull.jsx~line 563: same expression for the PR page's inline diffTypeError: Cannot read properties of null (reading 'patch')matches exactly.useDatafetcher (web/src/lib/data.jsstart), soentry.erroris set and the error goes to the global tray — but the page component'sgetView()staysundefined, soPullFiles.jsx~line 66 renders itsfallback={<p class="muted">loading diff…</p>}indefinitely while the heading's(getView()?.files ?? []).lengthrenders (0). The heading count is therefore a lie: 0 means "failed to load", not "no files changed".startIfStale, becauseentry.atis stamped on error too) or a full page reload. There is no in-page Retry control, and if the failure is deterministic (e.g. server 500 on a malformed range) the page loops silently between tray toasts and the spinner.Architecture notes
pulls.diffshould read the body as text (raw: truepath incore.js_textResponse), since the endpoint is text/plain by spec (docs/go/07_api.md§9.5 — "text/plain unifiedbase...headpatch"). The pages'typeof res === "string"branch then becomes the live path and theres.patch ?? res.difffallbacks become dead defensive code — either keep them null-safe or remove them.web/src/pages/Commit.jsx~line 263,parsePatchFiles(data.patch ?? "")) rides the JSON commits endpoint and already tolerates a nullpatch— but not a nulldata; the same defensive normalization should apply there so one commit's diff failure can't wedge its page the same way.parsePatchFiles(web/src/lib/diff.js) already returns{files: []}for an empty string — the honest "0 files / empty diff" state falls out for free once the SDK stops resolving tonull.EMPTY_MARKERguide indata.js, issue #209); the Retry control can driveuseData's refetch by dropping/re-staring the entry — no new data-layer API is strictly required, but a smallinvalidate(key)/refetch(key)helper inlib/data.jsis the clean seam if the entry keeps its error.Acceptance criteria
pulls.diffin the SDK resolves to the patch string (text/plain read as text); an empty body resolves to"", notnull; a server error still throwsReposError.loading diff…and never a TypeError.Pull.jsx) and the per-commit diff (Commit.jsx) are null-safe the same way.DiffBodyis unchanged.Fix is up: #526 (branch fix/issue-520) — SDK pulls.diff reads text/plain via raw:true; pages normalize null-safe, count honestly, and render error + Retry; Commit.jsx hardened the same way. Tests 10/10 new, full suite 1180/1179 (1 pre-existing live-server smoke fail, verified on pristine main), vite+esbuild green, no backend change.
Review of PR #526 (fix/issue-520) — verified in scratch worktree /tmp/pr526 @
b19ff3a(since removed). No browser (node tests + reasoning only — noted explicitly per task).FINDINGS (all 7 review points pass, no fixes needed):
TESTS (scratch worktree, node_modules symlinked from main): targeted pull-diff-520.test.js 10/10 pass; FULL node --test web/test/unit/*.test.js: 1180 total / 1179 pass / 1 fail — the 1 fail is smoke.test.js live-server /setup 403, verified IDENTICAL on pristine main (3 tests / 1 fail there too), i.e. pre-existing, zero PR-caused. vite build green (2.58s; chunk-size warning only, pre-existing). esbuild SDK bundle not re-run (no SDK dep/shape change beyond one flag; vite bundle includes SDK). Mobile: error/Retry block is in-flow card + button with existing responsive classes — reasoned, no browser per task.
MERGE RECOMMENDATION: ready to merge. No push made (nothing to fix). Main worktree untouched (still clean on main).
Fixed by PR #526 (review clean — all 7 checks pass, crash sim confirms the fix, no eternal spinner), merged. Closing.