fix(feedback): answer JSON on every path so failures stop reading as network errors - #274
Open
forcingfx wants to merge 1 commit into
Open
fix(feedback): answer JSON on every path so failures stop reading as network errors#274forcingfx wants to merge 1 commit into
forcingfx wants to merge 1 commit into
Conversation
…network errors
1. What: submitting the in-app feedback form failed with "Could not send
feedback. Check your connection." — a message that points the user at their
network when the network is fine. No feedback issue has been filed since
2026-05-17.
2. Why: two defects that compound.
The dialog called `fetch` and `res.json()` inside one `try`, so a *failed
parse* was indistinguishable from a *failed request*. Any response that is
not JSON — a crashed function's HTML 500, a gateway timeout page, a
misrouted request — landed in the connection-error branch, discarding the
status code that identifies which layer actually broke.
The handler produced exactly such responses. `body.message?.trim()` ran
straight off `JSON.parse` output, but `null`, arrays, bare strings, numbers
and `{"message": 42}` are all valid JSON — each threw a TypeError out of the
handler. A rejected handler promise is not a response, so the platform
substitutes its own HTML error page. The method guard returned plain-text
"Method not allowed" for the same reason: unparseable by the client.
Neither had a smoke test or an E2E, so three months of silence went unseen.
3. Fix:
- `readTrimmedString` narrows untrusted payload fields to string before use,
keeping every malformed payload on the 400-JSON path. A non-string email
is now treated as absent, matching the existing whitespace-email rule.
- The method guard returns `jsonResponse` like every other branch.
- `submitFeedback` separates transport from parsing: only a rejected `fetch`
reports a connection problem; an answered request reports the server's own
`error`, falling back to `server error <status>`.
4. Prevention:
- Handler: a table-driven case per malformed payload asserting 400 JSON
rather than a throw, plus the 405-is-JSON contract.
- Dialog: an HTML 500 must surface the status and must NOT mention the
connection; a failed submit keeps the dialog open with the text intact.
- `tests/smoke/feedback.test.ts` exercises the live endpoint without filing
an issue (every request is rejected at validation) and fails loudly if the
GitHub credentials go missing — the check that was absent all along.
- The JSON-on-every-path contract is recorded in docs/architecture.md and
CLAUDE.md's Key Patterns.
Verified in Chromium against the running app: HTML 500 → "server error 500",
aborted request → "Check your connection.", 200 → success toast.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mG1HfKCGqPXXQ9m38W1aj
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What: submitting the in-app feedback form failed with "Could not send
feedback. Check your connection." — a message that points the user at their
network when the network is fine. No feedback issue has been filed since
2026-05-17.
Why: two defects that compound.
The dialog called
fetchandres.json()inside onetry, so a failedparse was indistinguishable from a failed request. Any response that is
not JSON — a crashed function's HTML 500, a gateway timeout page, a
misrouted request — landed in the connection-error branch, discarding the
status code that identifies which layer actually broke.
The handler produced exactly such responses.
body.message?.trim()ranstraight off
JSON.parseoutput, butnull, arrays, bare strings, numbersand
{"message": 42}are all valid JSON — each threw a TypeError out of thehandler. A rejected handler promise is not a response, so the platform
substitutes its own HTML error page. The method guard returned plain-text
"Method not allowed" for the same reason: unparseable by the client.
Neither had a smoke test or an E2E, so three months of silence went unseen.
Fix:
readTrimmedStringnarrows untrusted payload fields to string before use,keeping every malformed payload on the 400-JSON path. A non-string email
is now treated as absent, matching the existing whitespace-email rule.
jsonResponselike every other branch.submitFeedbackseparates transport from parsing: only a rejectedfetchreports a connection problem; an answered request reports the server's own
error, falling back toserver error <status>.Prevention:
rather than a throw, plus the 405-is-JSON contract.
connection; a failed submit keeps the dialog open with the text intact.
tests/smoke/feedback.test.tsexercises the live endpoint without filingan issue (every request is rejected at validation) and fails loudly if the
GitHub credentials go missing — the check that was absent all along.
CLAUDE.md's Key Patterns.
Verified in Chromium against the running app: HTML 500 → "server error 500",
aborted request → "Check your connection.", 200 → success toast.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012mG1HfKCGqPXXQ9m38W1aj