DEV Community

Cover image for Fixing a null-body crash in the Formbricks survey SDK, found by Sentry
Lewis Sawe
Lewis Sawe Subscriber

Posted on

Fixing a null-body crash in the Formbricks survey SDK, found by Sentry

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
A survey widget should not be able to take down the page it is embedded on. This one could: a single API response with a null body threw an uncaught TypeError in the visitor's browser. Sentry's bot found it, filed it as a GitHub issue, and Seer pointed at the exact line. Here is the fix.

Project Overview

Formbricks is an open-source survey and experience-management platform. Websites and apps embed a small JavaScript SDK that loads a survey, shows it to a user, and posts the answers back to the Formbricks API. The SDK lives in the monorepo as two packages: @formbricks/js-core (the loader and command queue) and @formbricks/surveys (the survey renderer). Both talk to the backend through a shared makeRequest helper.

Bug Fix or Performance Improvement

I fixed issue #6581, a production crash that Sentry filed automatically:

Bug: API data is not always validated in the surveys package

TypeError: Cannot read properties of null (reading 'data')

The issue was opened automatically by sentry[bot], and its body carries the Sentry-captured (minified) stack trace plus a link to the source event, FORMBRICKS-CLOUD-3VE. Sentry did not just record this crash, it reported it.

The SDK calls makeRequest to load a workspace's environment state. That code parsed the HTTP response and immediately read .data off the result:

const json = (await response.json()) as ApiResponse;
// ...
const successResponse = json as ApiSuccessResponse<T>;
return ok(successResponse.data);
Enter fullscreen mode Exit fullscreen mode

Two things go wrong here:

  1. response.json() on a body of literal null returns JavaScript null. Reading null.data throws TypeError: Cannot read properties of null (reading 'data'). The error path had the same problem one line up: errorResponse.code on a null body throws reading 'code'.
  2. response.json() is not guarded at all. A non-JSON body (an empty response, or an HTML error page from a proxy or CDN) makes it throw an unhandled SyntaxError.

Both happen on the survey load path, in the visitor's browser, where there is no try/catch above them. Any visitor whose environment fetch returns a null or non-JSON body (a partial response, or an HTML error page from a proxy or CDN) hits this: the survey does not just fail to open, the SDK throws and the crash propagates into the host page. This is the exact issue Sentry surfaced as FORMBRICKS-CLOUD-3VE.

The same makeRequest is duplicated in @formbricks/surveys (src/lib/utils.ts) with the identical bug, so I fixed both.

Code

PR (on my fork, merged): https://github.com/lewisawe/formbricks/pull/1

Formbricks' contributing guide says they do not take unsolicited community PRs except in rare cases, and the challenge accepts a fork PR, so I opened and merged this on my own fork instead of adding noise upstream.

Files changed:

  • packages/js-core/src/lib/common/api.ts
  • packages/js-core/src/lib/common/tests/api.test.ts
  • packages/surveys/src/lib/utils.ts
  • packages/surveys/src/lib/utils.test.ts

The core change:

const response = res.data;

// Guard 1: never let response.json() throw an unhandled SyntaxError.
const parsedJson = await wrapThrowsAsync(() => response.json() as Promise<ApiResponse>)();

if (!parsedJson.ok) {
  return err({
    code: "network_error",
    status: response.status,
    message: "Failed to parse response body as JSON",
    url,
  });
}

const json = parsedJson.data;

if (!response.ok) {
  const errorResponse = json as ApiErrorResponse | null;
  return err({
    code: errorResponse?.code === "forbidden" ? "forbidden" : "network_error",
    status: response.status,
    message: errorResponse?.message || "Something went wrong",
    url,
    ...(Object.keys(errorResponse?.details ?? {}).length > 0 && { details: errorResponse?.details }),
  });
}

// Guard 2: validate the data envelope before reading `.data`.
const successResponse = json as ApiSuccessResponse<T> | null;

if (successResponse === null || successResponse.data === undefined || successResponse.data === null) {
  return err({
    code: "network_error",
    status: response.status,
    message: "Received invalid response data from the server",
    url,
  });
}

return ok(successResponse.data);
Enter fullscreen mode Exit fullscreen mode

My Improvements

I kept the change small and inside the existing error model. makeRequest already returns a Result<T, ApiErrorResponse> and the codebase has a wrapThrowsAsync helper that turns a throwing async call into a Result. I used both so the fix reads like the rest of the file:

  • Parsing can't crash anymore. response.json() runs through wrapThrowsAsync. Bad JSON becomes a network_error Result, not an exception.
  • The error path is null-safe. Optional chaining on errorResponse means a null error body no longer throws while trying to build the error.
  • The success path validates before reading. A null body, a non-object body, or a missing data field returns a typed network_error instead of throwing. Callers already handle failed Results, so the SDK degrades quietly instead of crashing the page.

For tests I extended the existing suites rather than adding new files, matching the project's convention of colocated unit tests:

  • packages/js-core: 11 to 15 tests in api.test.ts.
  • packages/surveys: added a makeRequest block, 76 to 80 tests in utils.test.ts.

Verification:

  • With the fix: 295/295 js-core tests pass, 80/80 surveys utils tests pass, tsc and ESLint are clean, and turbo build builds both packages.
  • Run against the actual unmodified Formbricks module (I reverted the fix with git stash and re-ran), the four new tests fail with exactly the reported errors: Cannot read properties of null (reading 'data'), reading 'code', and the unhandled SyntaxError. This is the definitive proof, it pins the real bug in the real code, not a rephrasing. The Sentry harness below uses a faithful standalone copy of that same function so it can run without building the monorepo.

Best Use of Sentry

Sentry is what made this bug findable. It was auto-filed by sentry[bot] as issue #6581, linking the source event FORMBRICKS-CLOUD-3VE, and the grouped title, Cannot read properties of null (reading 'data'), is specific enough to trace straight to the .data read in makeRequest.

To confirm the root cause and verify the fix, I built a small Sentry-instrumented harness that reproduces the crash without needing production traffic (the challenge's suggested workflow). It initializes @sentry/node, overrides fetch to return a 200 with a null body, and calls a faithful copy of the pre-fix makeRequest. Running it throws the same TypeError, and Sentry captures it:

=== Running makeRequest in "buggy" mode against a null-body 200 response ===
UNHANDLED CRASH: TypeError: Cannot read properties of null (reading 'data')
[Sentry capture] TypeError: Cannot read properties of null (reading 'data')
Enter fullscreen mode Exit fullscreen mode

The screenshots below are from that harness (the stack frames show buggy-api.mjs), not from Formbricks' production Sentry, which I have no access to. Here is the crash grouped in Sentry:

Sentry issue: TypeError Cannot read properties of null (reading 'data'

The stack trace points straight at the unguarded .data read in makeRequest:

Stack trace at makeRequest, the successResponse.data read on line 52

I enriched each event with tags and context, so the issue is traceable back to the original report:

Sentry tags: formbricks_issue=6581, sentry_issue=FORMBRICKS-CLOUD-3VE, package, fn

Then I ran Seer, Sentry's AI root-cause analysis, on the captured issue. It independently reached the same conclusion I did: makeRequest parses the body and reads .data with no null guard, so a 200 with a null body throws. Seer located the exact line, named the fix (add a null check on the parsed response before reading .data), and summed up the root cause in one line: the client trusts the JSON body is a non-null object, but the server can return null.

Seer root-cause analysis pointing at the missing null guard

Seer's suggested fix and one-line root cause

After the fix, the formbricks-6581@fixed release runs the same null-body response through makeRequest. The TypeError is gone. But rather than let the failure vanish into silence, I instrument the recovery: the handled path emits a warning-level Sentry event tagged outcome=handled_degrade, so Sentry shows the failure mode as controlled instead of fatal. The same input that was an error-level crash before the fix is now a warning-level "handled" signal:

=== Running makeRequest in "fixed" mode against a null-body 200 response ===
makeRequest returned a Result (no crash): {"ok":false,"error":{"code":"network_error","status":200,...}}
Graceful failure -> code="network_error", message="Received invalid response data from the server"
[Sentry capture] Survey load received invalid response data; degraded gracefully (no crash)
Enter fullscreen mode Exit fullscreen mode

Stated as an invariant in Sentry terms: for this input, makeRequest must never raise an error-level event; at worst it emits a handled warning. That is the difference the fix makes, made observable.

Sentry warning event on the fixed release: outcome=handled_degrade, no crash

Because the real crash happens in a visitor's browser, I also built a browser reproduction that loads the survey with @sentry/browser and the Replay integration, so Sentry records a Session Replay of the page crashing on survey load. You can watch the exact sequence: the widget starts loading, the null-body response comes back, and the survey fails.

Sentry Session Replay of the survey-load crash in the browser

To be precise about the roles: Sentry found this bug (the sentry[bot] issue), Seer explained it, and Session Replay, the trace, and the handled-degrade signal verified the fix. Code inspection and the failing tests are what proved the root cause. I am not claiming Sentry wrote the fix, I am showing where each Sentry tool earned its place in the loop.

Sentry tools used: Error Monitoring (found and grouped the crash), Session Replay (recorded the crash in the browser, the SDK's real environment), Breadcrumbs and custom Tags/Context (traced the event back to issue #6581 / FORMBRICKS-CLOUD-3VE), Distributed Tracing (each call wrapped in a named makeRequest span), and Seer (AI root-cause analysis that independently confirmed the fix).

The reproduction harness (Node and browser) is public here: https://github.com/lewisawe/sentry-repro

Top comments (0)