Skip to content

Commit f794dbd

Browse files
authored
fix(nextjs): Unwrap req and res if necessary when instrumenting server (#4467)
In vercel/next.js#32999 (released as part of 12.0.9), Next.js made an internal change, so that it now wraps the raw `http.IncomingMessage` and `http.ServerResponse` objects which get passed to the main server request handler in `NodeNextRequest` and `NodeNextResponse` objects, respectively. We therefore need to unwrap them before we can use them. This does that unwrapping (if necessary; older versions of nextjs still don't need it) and fixes the relevant types. Fixes #4463 Fixes vercel/next.js#33726
1 parent 8cbcff2 commit f794dbd

File tree

1 file changed

+38
-15
lines changed

1 file changed

+38
-15
lines changed

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,31 @@ interface Server {
3434
publicDir: string;
3535
}
3636

37-
export interface NextRequest extends http.IncomingMessage {
37+
export type NextRequest = (
38+
| http.IncomingMessage // in nextjs versions < 12.0.9, `NextRequest` extends `http.IncomingMessage`
39+
| {
40+
_req: http.IncomingMessage; // in nextjs versions >= 12.0.9, `NextRequest` wraps `http.IncomingMessage`
41+
}
42+
) & {
3843
cookies: Record<string, string>;
3944
url: string;
4045
query: { [key: string]: string };
4146
headers: { [key: string]: string };
4247
body: string | { [key: string]: unknown };
43-
}
44-
type NextResponse = http.ServerResponse;
48+
method: string;
49+
};
50+
51+
type NextResponse =
52+
// in nextjs versions < 12.0.9, `NextResponse` extends `http.ServerResponse`
53+
| http.ServerResponse
54+
// in nextjs versions >= 12.0.9, `NextResponse` wraps `http.ServerResponse`
55+
| {
56+
_res: http.ServerResponse;
57+
};
4558

4659
// the methods we'll wrap
4760
type HandlerGetter = () => Promise<ReqHandler>;
48-
type ReqHandler = (req: NextRequest, res: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise<void>;
61+
type ReqHandler = (nextReq: NextRequest, nextRes: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise<void>;
4962
type ErrorLogger = (err: Error) => void;
5063
type ApiPageEnsurer = (path: string) => Promise<void>;
5164
type PageComponentFinder = (
@@ -205,10 +218,16 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
205218
// add transaction start and stop to the normal request handling
206219
const wrappedReqHandler = async function (
207220
this: Server,
208-
req: NextRequest,
209-
res: NextResponse,
221+
nextReq: NextRequest,
222+
nextRes: NextResponse,
210223
parsedUrl?: url.UrlWithParsedQuery,
211224
): Promise<void> {
225+
// Starting with version 12.0.9, nextjs wraps the incoming request in a `NodeNextRequest` object and the outgoing
226+
// response in a `NodeNextResponse` object before passing them to the handler. (This is necessary here but not in
227+
// `withSentry` because by the time nextjs passes them to an API handler, it's unwrapped them again.)
228+
const req = '_req' in nextReq ? nextReq._req : nextReq;
229+
const res = '_res' in nextRes ? nextRes._res : nextRes;
230+
212231
// wrap everything in a domain in order to prevent scope bleed between requests
213232
const local = domain.create();
214233
local.add(req);
@@ -220,23 +239,23 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
220239
const currentScope = getCurrentHub().getScope();
221240

222241
if (currentScope) {
223-
currentScope.addEventProcessor(event => parseRequest(event, req));
242+
currentScope.addEventProcessor(event => parseRequest(event, nextReq));
224243

225244
// We only want to record page and API requests
226-
if (hasTracingEnabled() && shouldTraceRequest(req.url, publicDirFiles)) {
245+
if (hasTracingEnabled() && shouldTraceRequest(nextReq.url, publicDirFiles)) {
227246
// If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
228247
let traceparentData;
229-
if (req.headers && isString(req.headers['sentry-trace'])) {
230-
traceparentData = extractTraceparentData(req.headers['sentry-trace'] as string);
248+
if (nextReq.headers && isString(nextReq.headers['sentry-trace'])) {
249+
traceparentData = extractTraceparentData(nextReq.headers['sentry-trace'] as string);
231250
logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
232251
}
233252

234253
// pull off query string, if any
235-
const reqPath = stripUrlQueryAndFragment(req.url);
254+
const reqPath = stripUrlQueryAndFragment(nextReq.url);
236255

237256
// requests for pages will only ever be GET requests, so don't bother to include the method in the transaction
238257
// name; requests to API routes could be GET, POST, PUT, etc, so do include it there
239-
const namePrefix = req.url.startsWith('/api') ? `${(req.method || 'GET').toUpperCase()} ` : '';
258+
const namePrefix = nextReq.url.startsWith('/api') ? `${nextReq.method.toUpperCase()} ` : '';
240259

241260
const transaction = startTransaction(
242261
{
@@ -245,8 +264,12 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
245264
metadata: { requestPath: reqPath },
246265
...traceparentData,
247266
},
248-
// extra context passed to the `tracesSampler`
249-
{ request: req },
267+
// Extra context passed to the `tracesSampler` (Note: We're combining `nextReq` and `req` this way in order
268+
// to not break people's `tracesSampler` functions, even though the format of `nextReq` has changed (see
269+
// note above re: nextjs 12.0.9). If `nextReq === req` (pre 12.0.9), then spreading `req` is a no-op - we're
270+
// just spreading the same stuff twice. If `nextReq` contains `req` (12.0.9 and later), then spreading `req`
271+
// mimics the old format by flattening the data.)
272+
{ request: { ...nextReq, ...req } },
250273
);
251274

252275
currentScope.setSpan(transaction);
@@ -270,7 +293,7 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
270293
}
271294
}
272295

273-
return origReqHandler.call(this, req, res, parsedUrl);
296+
return origReqHandler.call(this, nextReq, nextRes, parsedUrl);
274297
});
275298
};
276299

0 commit comments

Comments
 (0)