Sustained deviation on cf.do.namespace 03bea87799914e00be8eb9f49b1f4d44 - #1664
Conversation
The MCP request handler surfaced failures only through createMcpHandler's
onerror callback as console.error("MCP request error:", error). The SDK
funnels non-Error throws through new Error(String(value)) and Cloudflare's
console renders an Error as its stack alone, so the log line carried no
message whenever the thrown value was empty or not a well-formed Error. When
this error class spiked, operators had a stack with no cause and nothing in
Sentry/otel.
Add describeThrownError() which renders any thrown value (Error, empty-message
Error, non-Error, object, primitive) into a single always-informative line,
including status/code and a chained cause where present, and use it in the
onerror reporter. The exact 'MCP request error' prefix is preserved so the
existing log-based monitoring keeps matching.
Co-authored-by: Dhravya Shah <dhravya@supermemory.com>
|
Claude finished @Dhravya's task in 1m 26s —— View job Review Complete
Summary: Clean PR that fixes a real production observability gap. The Score: 9/10 — Good to ship. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
supermemory-mcp | 34f9e06 | Sep 12 2026, 09:11 PM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
supermemory-app | 34f9e06 | Commit Preview URL Branch Preview URL |
Sep 12 2026, 09:11 PM |
There was a problem hiding this comment.
Overview: Adds a defensive error-description utility so MCP request failures are always logged with an informative message, fixing a production observability gap where errors were effectively blank.
Issues found: None — this looks good to ship.
The implementation is solid:
describeThrownError()handles all edge cases defensively: well-formed errors, empty-message errors, errors with status/code fields, chained causes, and non-Error throws (undefined, null, objects, strings, circular references)- Uses try/catch guards around
Reflect.getandJSON.stringifyto prevent the logging utility itself from throwing - Tests cover the exact incident scenario (empty-message Error) plus all the other edge cases
- Minimal scope — only changes the one
onerrorcallback that was producing blank logs
Score: 9/10
The hosted Supermemory MCP server was returning a burst of failed requests to connected clients (Claude, ChatGPT, and other MCP apps), and the on-call view of those failures was effectively blank. When something went wrong deep in a request, the server recorded only a bare stack trace with no message and sent nothing to our error-tracking or tracing tools, so anyone looking at the spike could see that requests were failing but had no way to tell what was actually breaking or who was affected.
Concretely, clients hitting these failures got a generic server error and could not complete memory searches, saves, or lookups during the affected requests, and operators had no signal to act on — the error count climbed while the cause stayed invisible. Merging this makes every one of those request failures self-describing in the logs, so the same spike now names its own cause and can be diagnosed and fixed instead of guessed at.
Before / After
Before: When a request failed inside the MCP handler past the auth gate, the server logged
MCP request error:followed by whatever was thrown. Because the underlying SDK wraps non-error throws and the runtime prints an error as its stack alone, the message was routinely dropped: an empty-message error produced a stack with no text, and a thrown value that wasn't a proper error produced almost nothing usable. Nothing reached error tracking or tracing for this class. An operator watching the error count rise saw it climb with no attached cause, no HTTP status, and no way to separate one failure mode from another.After: Every request failure is rendered into a single, always-informative line that carries a real cause. Well-formed errors keep their name, message, and stack; empty-message errors show an explicit placeholder instead of a blank; HTTP status and error codes are pulled out when present, a chained cause is summarized, and values thrown that aren't errors (objects, strings,
undefined,null) are described and serialized safely. An operator watching the same spike now reads what each failure actually is, so the incident is diagnosable from the logs alone rather than opaque.What changed
apps/mcp/src/server/request-error.tswithdescribeThrownError(), a pure function that turns any thrown value into a stable, informative description (name/message/stack,status/code, chained cause, and safe serialization of non-error and circular values).onerrorreporter inapps/mcp/src/server/index.ts. The exactMCP request errorprefix is preserved so the existing log-based monitoring for this metric keeps matching; only the previously-blank part is filled in.apps/mcp/src/server/request-error.test.tscovering the well-formed error, the empty-message error (the exact shape the incident produced), status/code extraction, custom error names, chained causes, andundefined/null/string/object/circular throws.Assumptions & scope
MCP request error(serveModern /createMcpHandleronerror) class, whose defining property is that the producing throw is elided from telemetry. Tracing the exact upstream throw was not possible from the available data precisely because it was never recorded — the SDK routes tool- and resource-handler failures into JSON-RPC error responses, so the errors reaching this reporter are the ones the codebase currently cannot see. This change removes that blind spot at the source: it does not suppress the signal (it strengthens it into something actionable) and it changes no configuration value or default, so no existing assertion needed to be loosened.console.error("…:", error)pattern also exists in the auth validators (API key validation error,OAuth token validation error); those were left unchanged here, butdescribeThrownError()is reusable if we later choose to harden them too.Checks run
bun run test:unit(apps/mcp) — pass (33 tests across 5 files, including the 10 new tests).bun run check-types(apps/mcp: widget build +tsc --noEmitfor both tsconfigs) — pass.bunx biome checkon the three changed files — pass (after applying Biome's formatter).turboruns (bun run build, repo-widecheck-types) were not run separately; the change is isolated toapps/mcpand the package-level typecheck/lint/tests above cover it.Fixes: Sustained deviation on cf.do.namespace 03bea87799914e00be8eb9f49b1f4d44
What caused this
Affected: Durable Object
supermemory-mcp_SupermemoryMCP· Cloudflare account47c2b4d598af9d423c06fc9f936226d5· MCP request handling errors at 26events (baseline 2.8events, 5.6σ above) · severity high · 1 dependent, ~100% of nearby trafficA check caught it, an investigation traced it, an autofix wrote the change. Each step links to its record.
fix_097688…This pull request originated from a Polylane autofix. Polylane investigated the issue and delegated the fix to Cursor, which authored this pull request.