-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(tanstackstart-react): Add wrappers for manual instrumentation of servers-side middlewares #18680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
| } | ||
|
|
||
| return originalServer.apply(thisArgServer, argsServer); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Span never ends when middleware throws or skips next()
The span created by startSpanManual is only ended inside getNextProxy when next() is called. If the middleware throws an error (e.g., authentication failure) or returns early without calling next(), the span will never be ended. This causes orphaned spans and potential memory leaks. The trpcMiddleware implementation demonstrates the correct pattern: wrapping the middleware execution in a try-catch and calling span.end() in both success and error paths. Additionally, per the review rules, captureException with a proper mechanism should be called when an error occurs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a legit problem. Maybe you can test throwing an error in the middleware to make sure nothing unexpected happens here.
node-overhead report 🧳Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
|
| } | ||
|
|
||
| return originalServer.apply(thisArgServer, argsServer); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a legit problem. Maybe you can test throwing an error in the middleware to make sure nothing unexpected happens here.
| return startSpanManual(getMiddlewareSpanOptions(options.name), (span: Span) => { | ||
| // The server function receives { next, context, request } as first argument | ||
| // We need to proxy the `next` function inside that object | ||
| const middlewareArgs = argsServer[0] as { next?: (...args: unknown[]) => unknown } | undefined; | ||
| if (middlewareArgs && typeof middlewareArgs === 'object' && typeof middlewareArgs.next === 'function') { | ||
| middlewareArgs.next = getNextProxy(middlewareArgs.next, span, prevSpan); | ||
| } | ||
|
|
||
| return originalServer.apply(thisArgServer, argsServer); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Middleware spans are not closed if next() is not called or an error occurs, leading to a memory leak.
Severity: CRITICAL
🔍 Detailed Analysis
The middleware instrumentation uses startSpanManual to create a span, but this span is only ended when the middleware's next() function is invoked. If a middleware short-circuits the request (a valid pattern for authentication or validation) or throws an error before calling next(), the span is never closed. This leads to an accumulation of unclosed spans in memory, causing a memory leak and performance degradation over time, as well as incomplete tracing data.
💡 Suggested Fix
Wrap the middleware execution logic within a try...finally block. The call to span.end() should be placed in the finally block to ensure that the span is always closed, regardless of whether the middleware completes successfully, throws an error, or short-circuits by not calling next().
🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: packages/tanstackstart-react/src/server/middleware.ts#L47-L56
Potential issue: The middleware instrumentation uses `startSpanManual` to create a span,
but this span is only ended when the middleware's `next()` function is invoked. If a
middleware short-circuits the request (a valid pattern for authentication or validation)
or throws an error before calling `next()`, the span is never closed. This leads to an
accumulation of unclosed spans in memory, causing a memory leak and performance
degradation over time, as well as incomplete tracing data.
Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8404895
This PR adds a middleware wrapper to the
tanstackstartSDK that allows users to add tracing to their application middleware. Eventually we will want to patch this automatically, but that is a bit tricky since it requires build-time magic. This API provides a manual alternative for now and can later still act as a fallback for cases where auto-instrumentation doesn't work.How it works
The wrapper patches the middleware
options.serverfunction that gets executed whenever a middleware is run. Each middleware invocation creates a span with:At first I had the issue that if multiple middlewares were used they would be nested (i.e. first middleware is parent of second etc.). This is because the middlewares call
next()to move down the middleware chain, so trivially starting a span for the middleware execution would actually create a span that would last for the current middleware and any middlewares that come after in the middleware chain. I fixed that by also proxyingnext(), where I end the middleware span and then also reattach the middleware spans to the parent request span instead of the previous middleware span.Usage
Tests
Added E2E tests for:
Screenshots from sample app
Using two global request middlewares:
Closes #18666