diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/static/route.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/static/route.ts new file mode 100644 index 000000000000..174fb171780e --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/static/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ result: 'static response' }); +} + +// This export makes it so that this route is always dynamically rendered (i.e Sentry will trace) +export const revalidate = 0; + +// This export makes it so that this route will throw an error if the Request object is accessed in some way. +export const dynamic = 'error'; diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 75a07d79b2c4..560a5911522f 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -94,3 +94,10 @@ test.describe('Edge runtime', () => { expect(routehandlerError.contexts?.runtime?.name).toBe('vercel-edge'); }); }); + +test('should not crash route handlers that are configured with `export const dynamic = "error"`', async ({ + request, +}) => { + const response = await request.get('/route-handlers/static'); + expect(await response.json()).toStrictEqual({ result: 'static response' }); +}); diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts index 9e3218959b34..ffca3dc8ff61 100644 --- a/packages/nextjs/src/common/types.ts +++ b/packages/nextjs/src/common/types.ts @@ -18,22 +18,19 @@ export type ServerComponentContext = { }; export interface RouteHandlerContext { - // TODO(v8): Remove - /** - * @deprecated The SDK will automatically pick up the method from the incoming Request object instead. - */ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; parameterizedRoute: string; // TODO(v8): Remove /** - * @deprecated The SDK will automatically pick up the `sentry-trace` header from the incoming Request object instead. + * @deprecated pass a complete `Headers` object with the `headers` field instead. */ sentryTraceHeader?: string; // TODO(v8): Remove /** - * @deprecated The SDK will automatically pick up the `baggage` header from the incoming Request object instead. + * @deprecated pass a complete `Headers` object with the `headers` field instead. */ baggageHeader?: string; + headers?: WebFetchHeaders; } export type VercelCronsConfig = { path?: string; schedule?: string }[] | undefined; diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index c03bb3db0dbf..95ba1cfb5e26 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; -import { tracingContextFromHeaders, winterCGRequestToRequestData } from '@sentry/utils'; +import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; import { isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; @@ -15,23 +15,16 @@ export function wrapRouteHandlerWithSentry any>( ): (...args: Parameters) => ReturnType extends Promise ? ReturnType : Promise> { addTracingExtensions(); // eslint-disable-next-line deprecation/deprecation - const { method, parameterizedRoute, baggageHeader, sentryTraceHeader } = context; + const { method, parameterizedRoute, baggageHeader, sentryTraceHeader, headers } = context; return new Proxy(routeHandler, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(async () => { const hub = getCurrentHub(); const currentScope = hub.getScope(); - let req: Request | undefined; - let reqMethod: string | undefined; - if (args[0] instanceof Request) { - req = args[0]; - reqMethod = req.method; - } - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTraceHeader, - baggageHeader, + sentryTraceHeader ?? headers?.get('sentry-trace') ?? undefined, + baggageHeader ?? headers?.get('baggage'), ); currentScope.setPropagationContext(propagationContext); @@ -40,11 +33,13 @@ export function wrapRouteHandlerWithSentry any>( res = await trace( { op: 'http.server', - name: `${reqMethod ?? method} ${parameterizedRoute}`, + name: `${method} ${parameterizedRoute}`, status: 'ok', ...traceparentData, metadata: { - request: req ? winterCGRequestToRequestData(req) : undefined, + request: { + headers: headers ? winterCGHeadersToDict(headers) : undefined, + }, source: 'route', dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index b09d35c98b4b..e8fafc18139d 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/nextjs'; +import type { WebFetchHeaders } from '@sentry/types'; // @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; @@ -34,12 +35,14 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' apply: (originalFunction, thisArg, args) => { let sentryTraceHeader: string | undefined | null = undefined; let baggageHeader: string | undefined | null = undefined; + let headers: WebFetchHeaders | undefined = undefined; // We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API try { const requestAsyncStore = requestAsyncStorage.getStore(); sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; + headers = requestAsyncStore?.headers; } catch (e) { /** empty */ } @@ -50,6 +53,7 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' parameterizedRoute: '__ROUTE__', sentryTraceHeader, baggageHeader, + headers, }).apply(thisArg, args); }, });