Skip to content

Commit 15443f7

Browse files
committed
Allow request body to be read in each interceptor
1 parent c168d2b commit 15443f7

File tree

12 files changed

+124
-34
lines changed

12 files changed

+124
-34
lines changed

packages/next/src/server/app-render/create-interceptor.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ export async function createInterceptor(
8080
}
8181

8282
if (!interceptorPromise) {
83-
interceptorPromise = interceptRequest(request)
83+
// We need to ensure that every interceptor gets a fresh request clone, so
84+
// that it can consume the request body without affecting subsequent
85+
// inteceptors, a route handler, or an action handler.
86+
interceptorPromise = interceptRequest(request.clone())
8487
}
8588

8689
return interceptorPromise

packages/next/src/server/async-storage/with-request-store.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from '../../server/app-render/work-unit-async-storage.external'
88
import type { RenderOpts } from '../app-render/types'
99
import type { WithStore } from './with-store'
10-
import { NextRequest } from '../web/spec-extension/request'
10+
import { isNextRequest, type NextRequest } from '../web/spec-extension/request'
1111
import type { __ApiPreviewProps } from '../api-utils'
1212

1313
import { FLIGHT_HEADERS } from '../../client/components/app-router-headers'
@@ -134,15 +134,14 @@ export const withRequestStore: WithStore<WorkUnitStore, RequestContext> = <
134134
draftMode?: DraftModeProvider
135135
} = {}
136136

137-
const nextRequest =
138-
req instanceof NextRequest
139-
? req
140-
: NextRequestAdapter.fromBaseNextRequest(
141-
req,
142-
res && isNodeNextResponse(res)
143-
? signalFromNodeResponse(res.destination)
144-
: undefined
145-
)
137+
const nextRequest = isNextRequest(req)
138+
? req
139+
: NextRequestAdapter.fromBaseNextRequest(
140+
req,
141+
res && isNodeNextResponse(res)
142+
? signalFromNodeResponse(res.destination)
143+
: undefined
144+
)
146145

147146
const store: RequestStore = {
148147
type: 'request',

packages/next/src/server/route-modules/app-route/module.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -938,17 +938,7 @@ function proxyNextRequest(request: NextRequest, workStore: WorkStore) {
938938
return (
939939
target[requestCloneSymbol] ||
940940
(target[requestCloneSymbol] = () =>
941-
new Proxy(
942-
// This is vaguely unsafe but it's required since NextRequest does not implement
943-
// clone. The reason we might expect this to work in this context is the Proxy will
944-
// respond with static-amenable values anyway somewhat restoring the interface.
945-
// @TODO we need to rethink NextRequest and NextURL because they are not sufficientlly
946-
// sophisticated to adequately represent themselves in all contexts. A better approach is
947-
// to probably embed the static generation logic into the class itself removing the need
948-
// for any kind of proxying
949-
target.clone() as NextRequest,
950-
nextRequestHandlers
951-
))
941+
new Proxy(target.clone(), nextRequestHandlers))
952942
)
953943
default:
954944
// The receiver arg is intentionally the same as the target to fix an issue with

packages/next/src/server/web/sandbox/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
415415
? input.url
416416
: String(input)
417417
validateURL(url)
418-
super(url, init)
418+
super(input, init)
419419
this.next = init?.next
420420
}
421421
}

packages/next/src/server/web/spec-extension/adapters/next-request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class NextRequestAdapter {
8686
let body: BodyInit | null = null
8787
if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
8888
// @ts-expect-error - this is handled by undici, when streams/web land use it instead
89-
body = request.body
89+
body = getRequestMeta(request, 'clonableBody')?.cloneBodyStream()
9090
}
9191

9292
return new NextRequest(createUrl(request, { isEdge: false }), {

packages/next/src/server/web/spec-extension/request.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextURL } from '../next-url'
33
import { toNodeOutgoingHttpHeaders, validateURL } from '../utils'
44
import { RemovedUAError, RemovedPageError } from '../error'
55
import { RequestCookies } from './cookies'
6+
import type { BaseNextRequest } from '../../base-http'
67

78
export const INTERNALS = Symbol('internal request')
89

@@ -22,7 +23,7 @@ export class NextRequest extends Request {
2223
const url =
2324
typeof input !== 'string' && 'url' in input ? input.url : String(input)
2425
validateURL(url)
25-
if (input instanceof Request) super(input, init)
26+
if (typeof input === 'object') super(input, init)
2627
else super(url, init)
2728
const nextUrl = new NextURL(url, {
2829
headers: toNodeOutgoingHttpHeaders(this.headers),
@@ -88,6 +89,10 @@ export class NextRequest extends Request {
8889
public get url() {
8990
return this[INTERNALS].url
9091
}
92+
93+
public clone() {
94+
return new NextRequest(super.clone())
95+
}
9196
}
9297

9398
export interface RequestInit extends globalThis.RequestInit {
@@ -98,3 +103,14 @@ export interface RequestInit extends globalThis.RequestInit {
98103
}
99104
signal?: AbortSignal
100105
}
106+
107+
/**
108+
* When `NextRequest` is used accross bundle boundaries, an `instanceof` check
109+
* will not work. Instead, this helper function can be used to discriminate
110+
* between `NextRequest` and `BaseNextRequest`.
111+
*/
112+
export function isNextRequest(
113+
req: NextRequest | BaseNextRequest
114+
): req is NextRequest {
115+
return 'nextUrl' in req
116+
}
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import { NextRequest } from 'next/server'
22
import { logWithTime, setTimeout } from '../../time-utils'
33

4-
export default async function interceptRoot(
4+
export default async function interceptSlot(
55
request: NextRequest
66
): Promise<void> {
7-
await logWithTime('SlotInterceptor', () => setTimeout(500))
7+
await logWithTime('SlotInterceptor', async () => {
8+
if (
9+
request.method === 'POST' &&
10+
request.headers.get('content-type').startsWith('multipart/form-data')
11+
) {
12+
// Consume the request body to ensure this doesn't break when also
13+
// consumed in the action handler.
14+
await request.formData()
15+
}
16+
17+
await setTimeout(500)
18+
})
819
}

test/e2e/app-dir/interceptors/app/api/interceptor.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@ import { logWithTime } from '../time-utils'
44
export default async function interceptRoot(
55
request: NextRequest
66
): Promise<void> {
7-
await logWithTime('ApiInterceptor', () => Promise.resolve())
7+
await logWithTime('ApiInterceptor', async () => {
8+
if (request.method === 'POST') {
9+
console.log(
10+
'ApiInterceptor',
11+
request.nextUrl.pathname,
12+
await request.json()
13+
)
14+
}
15+
})
816
}

test/e2e/app-dir/interceptors/app/api/nested/interceptor.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@ import { logWithTime } from '../../time-utils'
44
export default async function interceptRoot(
55
request: NextRequest
66
): Promise<void> {
7-
await logWithTime('ApiNestedInterceptor', () => Promise.resolve())
7+
await logWithTime('ApiNestedInterceptor', async () => {
8+
if (request.method === 'POST') {
9+
console.log(
10+
'ApiNestedInterceptor',
11+
request.nextUrl.pathname,
12+
await request.json()
13+
)
14+
}
15+
})
816
}

test/e2e/app-dir/interceptors/app/api/nested/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ export async function GET(request: NextRequest) {
55

66
return Response.json({ hello: 'world' })
77
}
8+
9+
export async function POST(request: NextRequest) {
10+
console.log('POST', request.url, await request.json())
11+
12+
return Response.json({ hello: 'world' })
13+
}

0 commit comments

Comments
 (0)