-
Notifications
You must be signed in to change notification settings - Fork 28.3k
/
Copy pathsandbox.ts
168 lines (147 loc) · 5.33 KB
/
sandbox.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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,
requestStore,
edgeSandboxNextRequestContext,
} from './context'
import { requestToBodyStream } from '../../body-streams'
import { NEXT_RSC_UNION_QUERY } from '../../../client/components/app-router-headers'
import type { ServerComponentsHmrCache } from '../../response-cache'
import {
getBuiltinRequestContext,
type BuiltinRequestContextValue,
} from '../../after/builtin-request-context'
export const ErrorSource = Symbol('SandboxError')
const FORBIDDEN_HEADERS = [
'content-length',
'content-encoding',
'transfer-encoding',
]
interface RunnerFnParams {
name: string
onError?: (err: unknown) => void
onWarning?: (warn: Error) => void
paths: string[]
request: NodejsRequestData
useCache: boolean
edgeFunctionEntry: Pick<EdgeFunctionDefinition, 'assets' | 'wasm' | 'env'>
distDir: string
incrementalCache?: any
serverComponentsHmrCache?: ServerComponentsHmrCache
}
type RunnerFn = (params: RunnerFnParams) => Promise<FetchEventResult>
/**
* Decorates the runner function making sure all errors it can produce are
* tagged with `edge-server` so they can properly be rendered in dev.
*/
function withTaggedErrors(fn: RunnerFn): RunnerFn {
if (process.env.NODE_ENV === 'development') {
const { getServerError } =
require('../../../client/components/react-dev-overlay/server/middleware-webpack') as typeof import('../../../client/components/react-dev-overlay/server/middleware-webpack')
return (params) =>
fn(params)
.then((result) => ({
...result,
waitUntil: result?.waitUntil?.catch((error) => {
// TODO: used COMPILER_NAMES.edgeServer instead. Verify that it does not increase the runtime size.
throw getServerError(error, 'edge-server')
}),
}))
.catch((error) => {
// TODO: used COMPILER_NAMES.edgeServer instead
throw getServerError(error, 'edge-server')
})
}
return fn
}
export async function getRuntimeContext(
params: Omit<RunnerFnParams, 'request'>
): Promise<EdgeRuntime<any>> {
const { runtime, evaluateInContext } = await getModuleContext({
moduleName: params.name,
onWarning: params.onWarning ?? (() => {}),
onError: params.onError ?? (() => {}),
useCache: params.useCache !== false,
edgeFunctionEntry: params.edgeFunctionEntry,
distDir: params.distDir,
})
if (params.incrementalCache) {
runtime.context.globalThis.__incrementalCache = params.incrementalCache
}
if (params.serverComponentsHmrCache) {
runtime.context.globalThis.__serverComponentsHmrCache =
params.serverComponentsHmrCache
}
for (const paramPath of params.paths) {
evaluateInContext(paramPath)
}
return runtime
}
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(':') : []
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, {
headers: {
'x-middleware-next': '1',
},
}),
}
}
const edgeFunction: (args: {
request: RequestData
}) => Promise<FetchEventResult> = (
await runtime.context._ENTRIES[`middleware_${params.name}`]
).default
const cloned = !['HEAD', 'GET'].includes(params.request.method)
? params.request.body?.cloneBodyStream()
: undefined
const KUint8Array = runtime.evaluate('Uint8Array')
const urlInstance = new URL(params.request.url)
urlInstance.searchParams.delete(NEXT_RSC_UNION_QUERY)
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 {
let result: FetchEventResult | undefined = undefined
const builtinRequestCtx: BuiltinRequestContextValue = {
...getBuiltinRequestContext(),
// FIXME(after):
// arguably, this is an abuse of "@next/request-context" --
// it'd make more sense to simply forward its existing value into the sandbox (in `createModuleContext`)
// but here we're using it to just pass in `waitUntil` regardless if we were running in this context or not.
waitUntil: params.request.waitUntil,
}
await edgeSandboxNextRequestContext.run(builtinRequestCtx, () =>
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)
}
})
)
if (!result) throw new Error('Edge function did not return a response')
return result
} finally {
await params.request.body?.finalize()
}
})