OUT-3750 | SyntaxError: Unexpected token 'A', "An error o"... is not valid JSON#1282
OUT-3750 | SyntaxError: Unexpected token 'A', "An error o"... is not valid JSON#1282arpandhakal wants to merge 1 commit into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a class of
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "OUT-3750 | Handle non-JSON API responses..." | Re-trigger Greptile |
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 (apiUrlresolves to the deployment's own URL) and then callres.json()directly.When the inner serverless function fails at the Vercel platform level (timeout / crash / dropped connection — not a handled app error, which
withErrorHandleralways returns as JSON), Vercel returns a non-JSON body —"An error occurred with this application.","Internal Server Error", or an empty response.JSON.parsethen throwsSyntaxError: 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/..., andPOST /detail/..., with three distinct non-JSON bodies.Change
Route these raw
res.json()reads through the existingfetchWithErrorHandler, which checksres.ok/status before parsing and retries transient failures with backoff:(home)/page.tsx—getAllWorkflowStates,getViewSettings_fetchers/TemplatesFetcher.tsx,WorkflowStateFetcher.tsx,AllTasksFetcher.tsxdetail/[task_id]/[user_type]/actions.ts—postComment,updateCommentThe 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
fetchWithErrorHandler+ retry), not the root-cause fix. The durable fix is removing the HTTP self-fetch loopback and calling the service layer directly (the detailloaders.tsalready does this). Tracked separately.configure-tasks-app/page.tsx,client/page.tsx,manage-templates/[template_id]/page.tsx.Testing
tsc --noEmitcleaneslintclean on changed files🤖 Generated with Claude Code