Skip to content

fix(feedback): answer JSON on every path so failures stop reading as network errors - #274

Open
forcingfx wants to merge 1 commit into
mainfrom
claude/feedback-form-submission-error-tzqk2h
Open

fix(feedback): answer JSON on every path so failures stop reading as network errors#274
forcingfx wants to merge 1 commit into
mainfrom
claude/feedback-form-submission-error-tzqk2h

Conversation

@forcingfx

Copy link
Copy Markdown
Owner
  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

…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
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
feedzero Ready Ready Preview Aug 18, 2026 5:18pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants