Skip to content

Commit 0eedddb

Browse files
authored
short-circuit OPTIONS requests to pages (vercel#65295)
Currently `OPTIONS` requests to page handlers (eg `pages/foo.tsx` or `app/foo/page.tsx`) will respond as though it's handling a `GET` request. There should be no reason for these routes to handle `OPTIONS` requests as the only valid option is `GET`. We do not need to special-case actions here because those will always be invoked from the same origin as the canonical browser URL. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> Closes NEXT-3305
1 parent c5a0878 commit 0eedddb

File tree

9 files changed

+171
-38
lines changed

9 files changed

+171
-38
lines changed

packages/next/src/server/base-server.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ import { getTracer, isBubbledError, SpanKind } from './lib/trace/tracer'
110110
import { BaseServerSpan } from './lib/trace/constants'
111111
import { I18NProvider } from './future/helpers/i18n-provider'
112112
import { sendResponse } from './send-response'
113-
import { handleInternalServerErrorResponse } from './future/route-modules/helpers/response-handlers'
113+
import {
114+
handleBadRequestResponse,
115+
handleInternalServerErrorResponse,
116+
} from './future/route-modules/helpers/response-handlers'
114117
import {
115118
fromNodeOutgoingHttpHeaders,
116119
normalizeNextQueryParam,
@@ -2466,46 +2469,59 @@ export default abstract class Server<
24662469

24672470
return null
24682471
}
2469-
} else if (isPagesRouteModule(routeModule)) {
2470-
// Due to the way we pass data by mutating `renderOpts`, we can't extend
2471-
// the object here but only updating its `clientReferenceManifest` and
2472-
// `nextFontManifest` properties.
2473-
// https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
2474-
renderOpts.nextFontManifest = this.nextFontManifest
2475-
renderOpts.clientReferenceManifest =
2476-
components.clientReferenceManifest
2477-
2478-
const request = isNodeNextRequest(req) ? req.originalRequest : req
2479-
const response = isNodeNextResponse(res) ? res.originalResponse : res
2480-
2481-
// Call the built-in render method on the module.
2482-
result = await routeModule.render(
2483-
// TODO: fix this type
2484-
// @ts-expect-error - preexisting accepted this
2485-
request,
2486-
response,
2487-
{
2488-
page: pathname,
2472+
} else if (
2473+
isPagesRouteModule(routeModule) ||
2474+
isAppPageRouteModule(routeModule)
2475+
) {
2476+
// An OPTIONS request to a page handler is invalid.
2477+
if (req.method === 'OPTIONS' && !is404Page) {
2478+
await sendResponse(req, res, handleBadRequestResponse())
2479+
return null
2480+
}
2481+
2482+
if (isPagesRouteModule(routeModule)) {
2483+
// Due to the way we pass data by mutating `renderOpts`, we can't extend
2484+
// the object here but only updating its `clientReferenceManifest` and
2485+
// `nextFontManifest` properties.
2486+
// https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
2487+
renderOpts.nextFontManifest = this.nextFontManifest
2488+
renderOpts.clientReferenceManifest =
2489+
components.clientReferenceManifest
2490+
2491+
const request = isNodeNextRequest(req) ? req.originalRequest : req
2492+
const response = isNodeNextResponse(res)
2493+
? res.originalResponse
2494+
: res
2495+
2496+
// Call the built-in render method on the module.
2497+
result = await routeModule.render(
2498+
// TODO: fix this type
2499+
// @ts-expect-error - preexisting accepted this
2500+
request,
2501+
response,
2502+
{
2503+
page: pathname,
2504+
params: opts.params,
2505+
query,
2506+
renderOpts,
2507+
}
2508+
)
2509+
} else {
2510+
const module = components.routeModule as AppPageRouteModule
2511+
2512+
// Due to the way we pass data by mutating `renderOpts`, we can't extend the
2513+
// object here but only updating its `nextFontManifest` field.
2514+
// https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
2515+
renderOpts.nextFontManifest = this.nextFontManifest
2516+
2517+
// Call the built-in render method on the module.
2518+
result = await module.render(req, res, {
2519+
page: is404Page ? '/404' : pathname,
24892520
params: opts.params,
24902521
query,
24912522
renderOpts,
2492-
}
2493-
)
2494-
} else if (isAppPageRouteModule(routeModule)) {
2495-
const module = components.routeModule as AppPageRouteModule
2496-
2497-
// Due to the way we pass data by mutating `renderOpts`, we can't extend the
2498-
// object here but only updating its `nextFontManifest` field.
2499-
// https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952
2500-
renderOpts.nextFontManifest = this.nextFontManifest
2501-
2502-
// Call the built-in render method on the module.
2503-
result = await module.render(req, res, {
2504-
page: is404Page ? '/404' : pathname,
2505-
params: opts.params,
2506-
query,
2507-
renderOpts,
2508-
})
2523+
})
2524+
}
25092525
} else {
25102526
throw new Error('Invariant: Unknown route module type')
25112527
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>foo page</div>
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const dynamic = 'force-dynamic'
2+
3+
export async function GET() {
4+
return Response.json({ foo: 'bar' })
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Root({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html>
4+
<body>{children}</body>
5+
</html>
6+
)
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
export default function Page() {
3+
return (
4+
<>
5+
<button
6+
onClick={async () => {
7+
const response = await fetch('/app-route', { method: 'OPTIONS' })
8+
console.log(await response.json())
9+
}}
10+
>
11+
Trigger Options Request (/app route)
12+
</button>
13+
<button
14+
onClick={async () => {
15+
const response = await fetch('/app-page', { method: 'OPTIONS' })
16+
console.log(await response.text())
17+
}}
18+
>
19+
Trigger Options Request (/app page)
20+
</button>
21+
<button
22+
onClick={async () => {
23+
const response = await fetch('/pages-page', { method: 'OPTIONS' })
24+
console.log(await response.text())
25+
}}
26+
>
27+
Trigger Options Request (/pages page)
28+
</button>
29+
<button
30+
onClick={async () => {
31+
const response = await fetch('/api/pages-api-handler', {
32+
method: 'OPTIONS',
33+
})
34+
console.log(await response.text())
35+
}}
36+
>
37+
Trigger Options Request (/pages API route)
38+
</button>
39+
</>
40+
)
41+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('options-request', () => {
4+
const { next } = nextTestSetup({
5+
files: __dirname,
6+
})
7+
8+
it.each(['/app-page', '/pages-page'])(
9+
'should return a 400 status code when invoking %s with an OPTIONS request',
10+
async (path) => {
11+
const res = await next.fetch(path, { method: 'OPTIONS' })
12+
expect(res.status).toBe(400)
13+
// There should be no response body
14+
expect(await res.text()).toBe('')
15+
}
16+
)
17+
18+
// In app router, OPTIONS is auto-implemented if not provided
19+
it('should respond with a 204 No Content when invoking an app route handler with an OPTIONS request', async () => {
20+
const res = await next.fetch('/app-route', { method: 'OPTIONS' })
21+
expect(res.status).toBe(204)
22+
// There should be no response body
23+
expect(await res.text()).toBe('')
24+
})
25+
26+
// In pages router, unless the handler explicitly handles OPTIONS, it will handle the request normally
27+
it('should respond with a 200 + response body when invoking a pages API route with an OPTIONS request', async () => {
28+
const res = await next.fetch('/api/pages-api-handler', {
29+
method: 'OPTIONS',
30+
})
31+
expect(res.status).toBe(200)
32+
// There should be no response body
33+
expect((await res.json()).message).toBe('Hello from Next.js!')
34+
})
35+
36+
it('should 404 for an OPTIONS request to a non-existent route', async () => {
37+
const res = await next.fetch('/non-existent-route', { method: 'OPTIONS' })
38+
expect(res.status).toBe(404)
39+
})
40+
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next'
2+
3+
type ResponseData = {
4+
message: string
5+
}
6+
7+
export default function handler(
8+
req: NextApiRequest,
9+
res: NextApiResponse<ResponseData>
10+
) {
11+
res.status(200).json({ message: 'Hello from Next.js!' })
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>bar</div>
3+
}

0 commit comments

Comments
 (0)