PostHog / PostHog/posthog

api.ts: dropped-connection body reads file per-endpoint error tracking issues instead of being classified as NetworkError

Open
#86,304 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
6h 51m
Merged PRs (30d)
232

Description

Bug Description

getJSONFromSuccessResponse in frontend/src/lib/api.ts treats a dropped connection as an application fault, while handleFetch — a few hundred lines down in the same file — correctly classifies the identical failure as a network error. The result is a large, permanently-noisy family of error tracking issues named Failed to read response body [<METHOD> <path>] (status N).

The asymmetry

When the browser tears down a connection mid-request, fetch (or the body read) rejects with a bare TypeError. There are two places that can catch it, depending on whether response headers had already arrived:

  • Headers not yet received → handleFetch (frontend/src/lib/api.ts:7519) catches the TypeError, runs classifyNetworkFailure(), emits client_request_failure with status: 0, and throws a NetworkError carrying the original error as cause. offline / navigating reasons are then dropped by dropUnactionableNetworkExceptions.
  • Headers received (2xx), body read fails → getJSONFromSuccessResponse (frontend/src/lib/api.ts:400-409) catches the same TypeError and throws a bare ApiError with no classification, no client_request_failure, and no cause.

Same underlying event, two different outcomes. The second path files a real error tracking issue every time.

Why it's so noisy

The thrown message embeds the request pathname, so every distinct API route produces its own fingerprint. This one code path has spawned hundreds of separate issues across the app — one per endpoint — and each new endpoint that ships creates a brand-new issue, which fires any "issue created" alerting. Volume skews heavily toward WebKit, consistent with a body-stream teardown rather than a per-endpoint defect. Discarding the original error also means the reports carry nothing useful for triage: a genuinely truncated body and a cancelled request are indistinguishable after the fact.

How to reproduce

Observed in production, single-session evidence:

  1. Navigate between scenes so several concurrent API requests are in flight.
  2. Have the connection drop while they are running (all requests on the connection die within the same few ms).
  3. Requests that had not yet received headers are logged as client_request_failure with status: 0 and failure_reason: network, and file no issue.
  4. The one request that had received its 200 headers surfaces as Failed to read response body [GET /api/...] (status 200) in error tracking, sourced at frontend/src/lib/api.ts:408.

In the case that prompted this, six requests died in the same ~70ms window alongside an SSE error; five were classified as network failures and the sixth — differing only in having received headers first — became a new error tracking issue.

Proposed fix

Route the body-read failure through the same classification handleFetch already uses:

  • In getJSONFromSuccessResponse, when response.text() rejects with a TypeError and it is not an AbortError, call classifyNetworkFailure(), emit captureClientRequestFailure({ status: 0, ... }), and throw new NetworkError(reason, error) so the original error is preserved as cause.
  • Keep ApiError for the genuine defect below it: the body arrived intact but is not valid JSON (Malformed JSON response).

This collapses the per-endpoint fingerprints into a single grouped NetworkError, drops the offline / navigating slice entirely via the existing dropUnactionableNetworkExceptions filter, and preserves the underlying error for the network reason that remains.

Worth adding a test alongside the existing coverage in frontend/src/lib/api.test.ts (see the NetworkError assertions around lines 370 and 423) for the "headers received, body read rejects" case.

Additional context

Raised from a Slack thread: https://posthog.slack.com/archives/C0B1RPLUSRZ/p1787222423292689?thread_ts=1787222423.292689&cid=C0B1RPLUSRZ

Relevant code:

  • frontend/src/lib/api.ts:400-409getJSONFromSuccessResponse, the unclassified throw
  • frontend/src/lib/api.ts:7502-7532handleFetch, the correct classification
  • frontend/src/lib/api-error.ts:170-198NETWORK_ERROR_MESSAGES, UNACTIONABLE_NETWORK_ERROR_MESSAGES, NetworkError
  • frontend/src/layout/navigation/SelfReadOnlyNotice/selfReadOnlyModeLogic.ts:84-97dropUnactionableNetworkExceptions

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in frontend/src/lib/api.ts at getJSONFromSuccessResponse and compare its body-read handling with handleFetch around lines 7502-7532. Review NetworkError and classification behavior in frontend/src/lib/api-error.ts, then add coverage in frontend/src/lib/api.test.ts for a rejected body read after headers arrive. Done means the failure is classified consistently, preserves its cause, and retains ApiError for malformed JSON.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.