Repository navigation
Conversation
Add recordServerError, annotateServerResponse, serverErrorResponse and getAnonymousVisitorId. A full-stack (TanStack Start) app's Worker uses them, through the app template's middleware, to report its server errors to Base44 in the X-B44-Function-Error response header (v2), only when serving asks with X-B44-Capture-Errors: 1. No TanStack dependency and no new runtime dependency. Tests: Vitest units, a contract fixture checked against apper's read_function_error(version=2), and an end-to-end TanStack Start app built with @cloudflare/vite-plugin and run in Miniflare and workerd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'t fit - The root .gitignore's lib/ rule had dropped the e2e app's src/lib/error-capture-middleware.js, so the app couldn't build. - Escaping can grow a 500-byte message to 3,000 bytes (\u0001 per control character), so with name and fn the header could miss 3,072 bytes even without frames, and no report was written. Cut the message to the most code points that fit instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Base44 alerts an app's owner when the app fails in production. A full-stack (TanStack Start) app runs its server code (server functions and server routes) in its own Cloudflare Worker. When that code fails, Base44 learns almost nothing:
Only the Worker sees the real error, so the Worker has to report it. These helpers put the report in a response header,
X-B44-Function-Error. It holds the error's name, its message and its raw stack frames, which are positions in the built Worker modules. Base44 maps those back to the app's source with the Worker's source maps. Base44's serving layer reads the header and removes it before the response reaches the visitor, so visitors get exactly what they got before.Capture is opt-in per request. Serving sends
X-B44-Capture-Errors: 1when it wants a report, and without it the helpers do nothing. They never throw into the request: if something goes wrong inside them, they behave as if capture were off.Why the SDK. Every full-stack app already depends on
@base44/sdk. Keeping the header format, size limits and stack parsing in one versioned package lets the platform change them with a version bump, instead of editing each app's code. The helpers don't depend on TanStack. The app template wraps them in TanStack middleware and handles the TanStack-specific cases itself: redirects,notFoundand thrownResponses.getAnonymousVisitorId()belongs to the same feature. The template's client sends the visitor's existing anonymous id on server-function calls, so Base44 can count how many signed-out visitors an error affects.No new runtime dependencies, and no existing export changes.
Exported
recordServerErrorkeeps the error's name, message and raw stack in a module-levelWeakMapkeyed by theRequest. A later call for the same request replaces the earlier record.fnis dropped unless it matches^[A-Za-z0-9_$.:/-]{1,200}$.annotateServerResponse:kind: "uncaught": at any status when the record hasfn, or at a 500 when it doesn't.kind: "response". The message comes from the JSON body (§16.3.2's rules: JSON media types only, at most 2 KB from aclone(), 50 ms deadline, then cancel), or else from the last recorded error. The name and frames come only from a recorded error whose message matches.Accept.new Response(response.body, response), so the body still streams.serverErrorResponsereturns a 500 with atext/plainbody ofInternal Server Error, plus the header when capture is on.getAnonymousVisitorIdreturns the id the SDK already stores and sends asX-Base44-Anonymous-Id(getAnalyticsSessionId(), underbase44_analytics_session_idinlocalStorage). It returnsnullwhen there is nowindow. No second id.Header, as the reader expects:
v: 2;nameat most 64 characters.message:toWellFormed()(with a fallback), then cut to 500 UTF-8 bytes on a code point.frames: at most 30 raw V8 frames, in stack order: named, anonymous andasyncframes. Eval and native frames are skipped;file://and leading/are stripped. A frame that fails the file pattern, contains.., or has a position outside 1 to 2³¹−1 is dropped.\uXXXX. At most 3,072 bytes: frames are dropped from the end until it fits. Escaping can still push a 500-byte message past the budget (control characters become 6 bytes each), so if dropping every frame isn't enough, the message is cut further, to the most code points that fit, rather than losing the report.Body of
serverErrorResponse: plain-textInternal Server Error. It's the reason phrase, so it says nothing about the app and is the same with capture on or off: the platform's own 500 page stands in for it in production anyway. Not JSON, so the body rules can never read a message back out of it. It alsoconsole.errors the error, since that error used to escape the Worker and reach its logs, and swallowing it into a 500 would otherwise drop it.What differs from apper #30612's draft
functionIddoesn't exist in the pinned TanStack versions (@tanstack/start-client-core1.170.15, underreact-start1.168.36): a function middleware getsserverFnMeta: { id, name, filename }. WithfunctionId,fnis alwaysundefined, so a server function's error, which TanStack answers with a 200, is never reported. The e2e caught this. Change the function middleware (template, andMIDDLEWARE_SOURCEin #30614's codemod) to:nameis the function's variable name (sendInvoice) and always matches thefnpattern.idis a 64-hex hash, the fallback.filenamecan contain(group)parentheses, which the pattern rejects, so it isn't used.fnat a status other than 500 isn't written asuncaught, because the reader would reject it. At 501 to 599 it's reported asresponse, using the recorded message; at any other status, nothing is written.serverErrorResponselogs the error (above).Signatures and names are otherwise as the draft uses them, including
getAnonymousVisitorId()inauth-middleware.js.Tests
Vitest (
tests/unit/server-errors.test.ts, 128 tests):WeakMapisolation;fetch()andResponse.redirect()immutable ones;Acceptpage route;fnvalidation;getAnonymousVisitorIdin Node and in a stubbed browser.Every header a test produces is also run through a port of the backend reader at the response's status.
Contract (
tests/fixtures/function-error-header-v2.json): 40 headers the reader must accept, with the fields it keeps, or reject.tests/unit/server-errors-contract.test.tspins the TS port to it.check-function-error-contract.pyruns the same fixture through apper's realread_function_error(..., version=2): 40/40 match against #30606's head. Apper can run it in CI withpython check-function-error-contract.py <apper>, or copy the fixture.End to end (
tests/worker-e2e/, new CI jobserver-error-capture-e2e): a minimal TanStack Start app on@cloudflare/vite-plugin. Versions are the template's lockfile pins:react-start1.168.36,start-server-core1.169.19,router-core1.171.16,@cloudflare/vite-plugin1.54.6,vite8.2.0,rolldown1.2.3,workerd1.20260908.1. The source maps config is the template's, and the middleware is a copy of the template's with fix 1. 8/8 pass:fn: "sendInvoice";server.handlersroute returns a 500Internal Server Errorwith the header, and the same 500 without it when capture is off;responsewith itsdetails;Accept: application/jsonis untouched;workerd, with modules named asbase44 site deployuploads them: the frames name the uploaded modules (assets/router-….js,assets/server-fns-….js,index.js), and each<file>.mapexists in the build.What workerd prints: a deployed Worker's frames carry the module name exactly as uploaded, e.g.
at chargeCard (assets/router-BL_ldMyJ.js:1177:14), so they need no normalizing. Under Miniflare they come out asfile:///<absolute path>/dist/server/assets/…, because Miniflare appends//# sourceURL=file://…to every module. That's a local-dev artifact, and why the frame-path check runs in plain workerd.The release must contain
RecordServerErrorOptions, in the next@base44/sdkrelease (0.8.54 if nothing else ships first; npm is at 0.8.53).^0.8.54) in the template'spackage.jsonand lockfile, intemplate_packages.py, and assdk_min_versionfor theupdate-base44-packagescodemod. It also needs fix 1 in the template middleware and in #30614's codemod.🤖 Generated with Claude Code