Skip to content

feat: log what a React Router request throws without its text, and read BOUNDA_ERROR_TEXT - #259

Merged
benatespina merged 8 commits into
mainfrom
feat/error-text-handle-error
Nov 9, 2026
Merged

benatespina merged 8 commits into
mainfrom
feat/error-text-handle-error

Conversation

@benatespina

@benatespina benatespina commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Closes the two gaps feat: keep the text of errors out of logs and spans unless runtime.telemetry.errorText includes it #252 left: React Router's default handleError logs the message and the stack of whatever a loader or an action rethrows, in production too; and on Cloudflare the dev server's "include" never reached the app, which lives in its Durable Object under a configuration bundled the same for every environment.
  • @bounda-dev/react-router returns a handleError that follows runtime.telemetry.errorText through errorFields; the React Router templates and examples/onboarding wire it in app/entry.server.tsx.
  • BOUNDA_ERROR_TEXT overrides errorText from the environment, so .dev.vars turns the text on in development on Cloudflare while a deployment keeps it out.

Changes

  • react-router: handleError from createBounda (Node and Cloudflare) and @bounda-dev/react-router/app (HandleBoundaErrorFunction, exported). It logs one line, request failed, with the method and the error's class and code; the message, the stack and the request's path only with "include". It skips aborted requests and unwraps what React Router throws itself (no route matching). Node follows the booted app's configuration, and before any boot BOUNDA_ERROR_TEXT, then createBounda({ errorText }) (the dev server passes "include"); it never waits for a boot in flight; logger createConsoleLogger() by default. Cloudflare follows BOUNDA_ERROR_TEXT in the Worker's env, then the configuration a request has already imported; workersLogger by default. The rule is resolved synchronously: under vite dev a Worker cancels an import still pending once its response is sent, so awaiting one logged nothing. The bounda() Vite plugin warns when app/entry.server.* is missing or exports no handleError.
  • core: one resolver for the precedence every host shares, resolveErrorText (a host's override, then BOUNDA_ERROR_TEXT, then the configuration, then the host's own default, then "omit"), and withErrorText, the configuration with it applied; ERROR_TEXT_VARIABLE names the variable. boot(), the CLI, the Durable Object, the Worker and both handleErrors resolve through it; the dev server's "include" is passed as the fallback, not copied. A value other than "omit" or "include" throws ConfigurationError where the app starts, and is passed over (ignoreInvalidEnv) where an app's failure is logged.
  • cli: reads it below --error-text, for printed failures and the app's logs, bounda migrate included; a bad value is reported like a project that cannot load. bounda status (feat: share a lease among the background jobs, start them over after a restore, and report their status #260) opens the text of the subscribers' failures under the resolved rule, --error-text or BOUNDA_ERROR_TEXT=include alike, through the call the access log records.
  • cloudflare: the Durable Object reads it from its env and applies it to the app; the Worker's 500 log follows it, and under a value it does not know still answers its 500 under the configuration's rule; workersLogger exported.
  • create-bounda: app/entry.server.tsx for Node (renderToPipeableStream) and Cloudflare (renderToReadableStream), React Router 8.5's default entry for each runtime with handleError exported and called from the render's onError; BOUNDA_ERROR_TEXT=include in .dev.vars, and only a comment in .dev.vars.example, so Deploy to Cloudflare never asks for it.
  • docs: observability (error text), configuration, CLI, personal data, Cloudflare adapter, and a "Logging errors" section in the React Router guide.

Tests

  • React Router's own createRequestHandler over a server build whose loader rethrows: the default logs the message (control); with "omit" neither the message nor the stack reaches the console or the logger; with "include" both do.
  • e2e of create-bounda: a request no route matches makes React Router call handleError; the dev server logs its text, react-router-serve does not; on Cloudflare vite dev and wrangler dev log it under .dev.vars, and wrangler dev without the line does not; the no-framework Worker logs the object's refusal with its text under .dev.vars.
  • A precedence table on resolveErrorText/withErrorText, and one per host: boot(), the CLI, the Durable Object built with an env of the test's own, the Worker, the Node host and both Cloudflare handleErrors.
  • Stryker: core 96.49%, cli 93.30%, react-router 99.56%, cloudflare 94.17%, create-bounda 94.57%; the survivors in changed lines are equivalent (a process?.env Node always has, now gone; the object's #app?.config for an alarm on an object whose app failed to start) or fixed.

Notes

  • pnpm check is green except astro check in docs, which fails locally on macOS on a clean main too (@astrojs/check not detected, then an emnapi crash); CI runs it.
  • After the release, sync the two Cloudflare template copies (pnpm templates:sync): their .dev.vars.example gains the comment.

Left out

Nothing.

@benatespina
benatespina force-pushed the feat/error-text-handle-error branch 2 times, most recently from a0962f9 to 49d2486 Compare November 9, 2026 22:27
…ad BOUNDA_ERROR_TEXT

React Router's default handleError logs the message and the stack of whatever a loader or an action rethrows, in production too, and a message may quote personal data. `handleError` from `createBounda` and `@bounda-dev/react-router/app` logs it under `runtime.telemetry.errorText`, and the React Router templates and the onboarding example wire it in `app/entry.server.tsx`.

On Cloudflare the configuration is the same bundle in every environment, so `BOUNDA_ERROR_TEXT` in the environment overrides `errorText`: `.dev.vars` turns the text on under `wrangler dev` and `vite dev` for the Durable Object and the Worker alike. `boot()` and the CLI read it too.
…_TEXT

The Worker's 500 path threw on a BOUNDA_ERROR_TEXT it does not know instead of answering; it now follows the configuration and leaves the refusal to the Durable Object. Node's handleError reads the variable before an app has booted, as boot() would, and no longer waits for a boot in flight. Both Cloudflare paths share one handleError setup, and HandleBoundaErrorFunction is exported.
Four hosts each spelled the same precedence, a host's override, then BOUNDA_ERROR_TEXT, then the configuration, then the host's default, and one drifting would reopen the leak. resolveErrorText and withErrorText in core hold it; boot(), the CLI, the Durable Object, the Worker and both handleErrors use them, with the dev server's "include" passed as a fallback.

handleError now resolves its rule synchronously: under vite dev on Cloudflare it awaited an import of the configuration that the Worker cancelled once the response was sent, so nothing was logged. The Cloudflare host reads the configuration a request has already imported.
@benatespina
benatespina force-pushed the feat/error-text-handle-error branch from 49d2486 to c5f24b6 Compare November 9, 2026 23:02
@benatespina
benatespina merged commit bd58c11 into main Nov 9, 2026
9 checks passed
@benatespina
benatespina deleted the feat/error-text-handle-error branch November 9, 2026 23:37
benatespina added a commit that referenced this pull request Nov 10, 2026
Integrates B4 (#260), A2b (#259) and B2 (#261): the forget ledger's intention
carries the categories a forgetting asks for and the reconciliation commits
again only those; the categories migrations follow B4's and B2's in each scope;
bounda status reports each retention.
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.

1 participant