Skip to content
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

fix: allow some recursion for middleware subrequests #60615

Merged
merged 9 commits into from
Feb 6, 2024
24 changes: 24 additions & 0 deletions packages/next/src/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ const NativeModuleMap = (() => {
return new Map(Object.entries(mods))
})()

export const requestStore = new AsyncLocalStorage<{
headers: Headers
}>()

/**
* Create a module cache specific for the provided parameters. It includes
* a runtime context, require cache and paths cache.
Expand Down Expand Up @@ -335,6 +339,19 @@ Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
}

init.headers = new Headers(init.headers ?? {})

// Forward subrequest header from incoming request to outgoing request
const store = requestStore.getStore()
if (
store?.headers.has('x-middleware-subrequest') &&
!init.headers.has('x-middleware-subrequest')
) {
init.headers.set(
'x-middleware-subrequest',
store.headers.get('x-middleware-subrequest') ?? ''
)
}

const prevs =
init.headers.get(`x-middleware-subrequest`)?.split(':') || []
const value = prevs.concat(options.moduleName).join(':')
Expand Down Expand Up @@ -385,6 +402,13 @@ Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
: String(input)
validateURL(url)
super(url, init)

const store = requestStore.getStore()
if (store) {
new Headers(init?.headers).forEach((value, key) => {
store.headers.set(key, value)
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we needing to augment the store from headers in a new Request(), shouldn't this only be done from the initial request event in the sandbox?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that does sound correct - should be able to remove this here then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed it was unnecessary, removed it 👍🏼

this.next = init?.next
}
}
Expand Down
38 changes: 27 additions & 11 deletions packages/next/src/server/web/sandbox/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NodejsRequestData, FetchEventResult, RequestData } from '../types'
import type { EdgeFunctionDefinition } from '../../../build/webpack/plugins/middleware-plugin'
import type { EdgeRuntime } from 'next/dist/compiled/edge-runtime'
import { getModuleContext } from './context'
import { getModuleContext, requestStore } from './context'
import { requestToBodyStream } from '../../body-streams'
import { NEXT_RSC_UNION_QUERY } from '../../../client/components/app-router-headers'

Expand Down Expand Up @@ -82,7 +82,14 @@ export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
const runtime = await getRuntimeContext(params)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
if (subrequests.includes(params.name)) {

const MAX_RECURSION_DEPTH = 5
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)

if (depth >= MAX_RECURSION_DEPTH) {
return {
waitUntil: Promise.resolve(),
response: new runtime.context.Response(null, {
Expand All @@ -108,17 +115,26 @@ export const run = withTaggedErrors(async function runWithTaggedErrors(params) {

params.request.url = urlInstance.toString()

const headers = new Headers()
for (const [key, value] of Object.entries(params.request.headers)) {
headers.set(key, value?.toString() ?? '')
}

try {
const result = await edgeFunction({
request: {
...params.request,
body:
cloned && requestToBodyStream(runtime.context, KUint8Array, cloned),
},
let result: FetchEventResult | undefined = undefined
await requestStore.run({ headers }, async () => {
result = await edgeFunction({
request: {
...params.request,
body:
cloned && requestToBodyStream(runtime.context, KUint8Array, cloned),
},
})
for (const headerName of FORBIDDEN_HEADERS) {
result.response.headers.delete(headerName)
}
})
for (const headerName of FORBIDDEN_HEADERS) {
result.response.headers.delete(headerName)
}
if (!result) throw new Error('Edge function did not return a response')
Comment on lines +124 to +137
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks very sketchy - idk how far up the store.run call can be hoisted though or if there a better way to do this...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoisting to this level seems alright as it looks like we only need to access it in the same Node.js context.

return result
} finally {
await params.request.body?.finalize()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createNextDescribe } from 'e2e-utils'

const bathPath = process.env.BASE_PATH ?? ''

createNextDescribe(
'app-routes-subrequests',
{
files: __dirname,
skipDeployment: true,
},
({ next }) => {
it('shortcuts after 5 subrequests', async () => {
expect(JSON.parse(await next.render(bathPath + '/'))).toEqual({
count: 5,
})
})
}
)
11 changes: 11 additions & 0 deletions test/e2e/app-dir/app-routes-subrequests/app/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextRequest, NextResponse } from 'next/server'

export const runtime = 'edge'

let count = 0

export const GET = async (req: NextRequest) => {
await fetch(req.nextUrl)
count++
return NextResponse.json({ count })
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/app-routes-subrequests/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const config = {
typescript: {
ignoreBuildErrors: true,
},
}

module.exports = config