Skip to content

OUT-3750 | SyntaxError: Unexpected token 'A', "An error o"... is not valid JSON#1282

Open
arpandhakal wants to merge 1 commit into
mainfrom
OUT-3750-fetch-with-error-handler
Open

OUT-3750 | SyntaxError: Unexpected token 'A', "An error o"... is not valid JSON#1282
arpandhakal wants to merge 1 commit into
mainfrom
OUT-3750-fetch-with-error-handler

Conversation

@arpandhakal

Copy link
Copy Markdown
Collaborator

What & why

Fixes OUT-3750 / Sentry TASKS-88.

The (home) page loaders, the _fetchers/* components, and the comment server actions fetch the app's own API over HTTP (apiUrl resolves to the deployment's own URL) and then call res.json() directly.

When the inner serverless function fails at the Vercel platform level (timeout / crash / dropped connection — not a handled app error, which withErrorHandler always returns as JSON), Vercel returns a non-JSON body — "An error occurred with this application.", "Internal Server Error", or an empty response. JSON.parse then throws SyntaxError: Unexpected token ... / Unexpected end of JSON input, and with no error boundary the whole render 500s.

The 4 Sentry events confirm this across GET /, GET /detail/..., and POST /detail/..., with three distinct non-JSON bodies.

Change

Route these raw res.json() reads through the existing fetchWithErrorHandler, which checks res.ok/status before parsing and retries transient failures with backoff:

  • (home)/page.tsxgetAllWorkflowStates, getViewSettings
  • _fetchers/TemplatesFetcher.tsx, WorkflowStateFetcher.tsx, AllTasksFetcher.tsx
  • detail/[task_id]/[user_type]/actions.tspostComment, updateComment

The comment create/update use retries=0 — they are non-idempotent mutations, and retrying a POST/PATCH whose response was lost could duplicate/re-apply the write. They still get the safe parse + clean error, just no retry.

Scope / follow-ups

  • This is the mitigation (fetchWithErrorHandler + retry), not the root-cause fix. The durable fix is removing the HTTP self-fetch loopback and calling the service layer directly (the detail loaders.ts already does this). Tracked separately.
  • Three other SSR pages have the same loopback pattern but weren't in the TASKS-88 events and are intentionally left out of this PR: configure-tasks-app/page.tsx, client/page.tsx, manage-templates/[template_id]/page.tsx.

Testing

  • tsc --noEmit clean
  • eslint clean on changed files

🤖 Generated with Claude Code

The (home) page loaders, the _fetchers components, and the comment
server actions fetched the app's own API over HTTP and called
res.json() directly. When the inner serverless function failed at the
Vercel platform level (timeout / crash / dropped connection) it
returned a non-JSON body ("An error occurred...", "Internal Server
Error", or empty), so JSON.parse threw and the render 500'd.

Route these reads through fetchWithErrorHandler, which checks
res.ok/status before parsing and retries transient failures. Comment
create/update use retries=0 since they are non-idempotent mutations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 2, 2026

Copy link
Copy Markdown

OUT-3750

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment Jun 2, 2026 6:27am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a class of SyntaxError: Unexpected token crashes that occurred when Vercel returned platform-level non-JSON error bodies (timeouts, crashes, dropped connections) and the app called res.json() directly on those responses. The fix routes all affected fetch calls through the existing fetchWithErrorHandler, which validates res.ok/status before attempting JSON parsing and retries transient failures with linear backoff.

  • Loaders and fetchers (getAllWorkflowStates, getViewSettings, AllTasksFetcher, TemplatesFetcher, WorkflowStateFetcher) now use fetchWithErrorHandler with the default retries=3, applying backoff delays of 500 ms, 1 s, and 1.5 s between attempts.
  • Comment mutations (postComment, updateComment) use fetchWithErrorHandler with retries=0 — they get safe JSON parsing and a clean error throw, but no retry, correctly preventing duplicate comment creation on a lost-but-succeeded write.
  • The PR is explicitly a mitigation; the durable fix (removing the HTTP self-fetch loopback in favour of direct service-layer calls) is tracked separately.

Confidence Score: 4/5

Safe to merge — changes are narrowly scoped and the logic is correct across both idempotent reads (with retry) and non-idempotent mutations (no retry).

All five call sites are migrated consistently and the generic type annotations match the expected API shapes. The retries=0 choice for postComment/updateComment is well-reasoned and prevents duplicate writes. The one thing worth keeping an eye on is fetchWithErrorHandler's 500 branch: it throws before reading the response body, so Vercel's actual error text is lost and the logged message is always the generic 'Internal server error.' — making post-incident debugging slightly harder than the non-500 error path. This is pre-existing behaviour, not introduced here, but it affects the observability of the very failure mode this PR is hardening against.

No changed files require special attention. The pre-existing fetchWithErrorHandler.ts (not modified in this PR) is the one place where a small improvement — reading the body on 500 to preserve Vercel's error text — would benefit future debugging.

Important Files Changed

Filename Overview
src/app/_fetchers/fetchWithErrorHandler.ts Pre-existing helper used by all changed files; correctly guards res.json() behind ok-check, but the status=500 branch discards the response body before throwing, losing Vercel's error text that could aid debugging
src/app/(home)/page.tsx getAllWorkflowStates and getViewSettings now routed through fetchWithErrorHandler with default retries=3; change is correct and type-safe
src/app/_fetchers/AllTasksFetcher.tsx getAllAccessibleTasks migrated from raw fetch+res.json() to fetchWithErrorHandler with correct generic type; clean change
src/app/_fetchers/TemplatesFetcher.tsx getAllTemplates migrated to fetchWithErrorHandler with correct generic type; clean change
src/app/_fetchers/WorkflowStateFetcher.tsx getAllWorkflowStates migrated to fetchWithErrorHandler with correct generic type; clean change
src/app/detail/[task_id]/[user_type]/actions.ts postComment and updateComment use fetchWithErrorHandler with retries=0 to avoid duplicate writes on retry; correct approach for non-idempotent mutations

Sequence Diagram

sequenceDiagram
    participant SSR as SSR Loader / Fetcher
    participant FEH as fetchWithErrorHandler
    participant API as Internal API (apiUrl)
    participant Vercel as Vercel Platform

    SSR->>FEH: "fetchWithErrorHandler<T>(url, opts, retries)"
    loop "attempt = 0..retries"
        FEH->>API: fetch(url, options)
        alt Vercel platform error (non-JSON body)
            API-->>Vercel: function crash / timeout
            Vercel-->>FEH: 500 / 502 / 503 + plain-text body
            FEH->>FEH: "res.status===500 → throw Error or !res.ok → res.text() → throw Error"
            note over FEH: caught by catch block
            FEH->>FEH: "wait 500*(attempt+1) ms if attempt < retries"
        else Success (200 OK + JSON)
            API-->>FEH: 200 OK + JSON body
            FEH->>FEH: res.json() → return data
            FEH-->>SSR: resolved T
        end
    end
    FEH-->>SSR: throw lastError (all attempts exhausted)

    note over SSR,FEH: Mutations (postComment, updateComment) use retries=0 → single attempt, then throw
Loading

Reviews (1): Last reviewed commit: "OUT-3750 | Handle non-JSON API responses..." | Re-trigger Greptile

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

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