Skip to content

Commit 6f43c90

Browse files
authored
feat(edge): adds AsyncLocalStorage support to the edge function sandbox (vercel#41622)
## 📖 Feature Adds `AsyncLocalStorage` as a global variable to any edge function (middleware, Edge API routes). Falls back to Node.js' implementation. ## 🧪 How to test 1. `pnpm build` 2. `pnpm testheadless --testPathPattern async-local`
1 parent bdc53ef commit 6f43c90

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AsyncLocalStorage } from 'async_hooks'
12
import type { AssetBinding } from '../../../build/webpack/loaders/get-module-build-info'
23
import {
34
decorateServerError,
@@ -286,6 +287,8 @@ Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),
286287

287288
Object.assign(context, wasm)
288289

290+
context.AsyncLocalStorage = AsyncLocalStorage
291+
289292
return context
290293
},
291294
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/* eslint-disable jest/valid-expect-in-promise */
2+
import { createNext } from 'e2e-utils'
3+
import { NextInstance } from 'test/lib/next-modes/base'
4+
import { fetchViaHTTP } from 'next-test-utils'
5+
6+
describe('edge api can use async local storage', () => {
7+
let next: NextInstance
8+
9+
const cases = [
10+
{
11+
title: 'a single instance',
12+
code: `
13+
export const config = { runtime: 'experimental-edge' }
14+
const storage = new AsyncLocalStorage()
15+
16+
export default async function handler(request) {
17+
const id = request.headers.get('req-id')
18+
return storage.run({ id }, async () => {
19+
await getSomeData()
20+
return Response.json(storage.getStore())
21+
})
22+
}
23+
24+
async function getSomeData() {
25+
try {
26+
const response = await fetch('https://example.vercel.sh')
27+
await response.text()
28+
} finally {
29+
return true
30+
}
31+
}
32+
`,
33+
expectResponse: (response, id) =>
34+
expect(response).toMatchObject({ status: 200, json: { id } }),
35+
},
36+
{
37+
title: 'multiple instances',
38+
code: `
39+
export const config = { runtime: 'experimental-edge' }
40+
const topStorage = new AsyncLocalStorage()
41+
42+
export default async function handler(request) {
43+
const id = request.headers.get('req-id')
44+
return topStorage.run({ id }, async () => {
45+
const nested = await getSomeData(id)
46+
return Response.json({ ...nested, ...topStorage.getStore() })
47+
})
48+
}
49+
50+
async function getSomeData(id) {
51+
const nestedStorage = new AsyncLocalStorage()
52+
return nestedStorage.run('nested-' + id, async () => {
53+
try {
54+
const response = await fetch('https://example.vercel.sh')
55+
await response.text()
56+
} finally {
57+
return { nestedId: nestedStorage.getStore() }
58+
}
59+
})
60+
}
61+
`,
62+
expectResponse: (response, id) =>
63+
expect(response).toMatchObject({
64+
status: 200,
65+
json: { id: id, nestedId: `nested-${id}` },
66+
}),
67+
},
68+
]
69+
70+
afterEach(() => next.destroy())
71+
72+
it.each(cases)(
73+
'cans use $title per request',
74+
async ({ code, expectResponse }) => {
75+
next = await createNext({
76+
files: {
77+
'pages/index.js': `
78+
export default function () { return <div>Hello, world!</div> }
79+
`,
80+
'pages/api/async.js': code,
81+
},
82+
})
83+
const ids = Array.from({ length: 100 }, (_, i) => `req-${i}`)
84+
85+
const responses = await Promise.all(
86+
ids.map((id) =>
87+
fetchViaHTTP(
88+
next.url,
89+
'/api/async',
90+
{},
91+
{ headers: { 'req-id': id } }
92+
).then((response) =>
93+
response.headers.get('content-type')?.startsWith('application/json')
94+
? response.json().then((json) => ({
95+
status: response.status,
96+
json,
97+
text: null,
98+
}))
99+
: response.text().then((text) => ({
100+
status: response.status,
101+
json: null,
102+
text,
103+
}))
104+
)
105+
)
106+
)
107+
const rankById = new Map(ids.map((id, rank) => [id, rank]))
108+
109+
const errors: Error[] = []
110+
for (const [rank, response] of responses.entries()) {
111+
try {
112+
expectResponse(response, ids[rank])
113+
} catch (error) {
114+
const received = response.json?.id
115+
console.log(
116+
`response #${rank} has id from request #${rankById.get(received)}`
117+
)
118+
errors.push(error as Error)
119+
}
120+
}
121+
if (errors.length) {
122+
throw errors[0]
123+
}
124+
}
125+
)
126+
})

0 commit comments

Comments
 (0)