Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 263853b

Browse filesBrowse files
mrstorkpieh
andauthored
fix: cookies set in middleware accessible during the same request (#2847)
* fix: allow cookies in middleware to be accessible during the same request * test: validate functionality of mergeMiddlewareCookies * ci: pin deno version to 1.x until builds are ready for 2.x * ci: add vendor step before running deno tests * chore: include reference link for cookie regex * fix: directives in cookies are joined by a semi-colon with a space * fix: make cookies available during first request on rewrite as well --------- Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
1 parent bc87632 commit 263853b
Copy full SHA for 263853b

File tree

Expand file treeCollapse file tree

4 files changed

+161
-19
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+161
-19
lines changed

‎.github/workflows/deno-test.yml

Copy file name to clipboardExpand all lines: .github/workflows/deno-test.yml
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
- name: Setup Deno
1212
uses: denoland/setup-deno@v1
1313
with:
14-
deno-version: vx.x.x
14+
deno-version: v1.x.x
15+
- name: Vendor Deno modules
16+
run: deno vendor edge-runtime/vendor.ts --output=edge-runtime/vendor --force
1517
- name: Test
1618
run: deno test -A edge-runtime/

‎edge-runtime/lib/middleware.test.ts

Copy file name to clipboard
+92Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { assertEquals } from 'https://deno.land/std@0.175.0/testing/asserts.ts'
2+
import { mergeMiddlewareCookies } from './middleware.ts'
3+
4+
const MIDDLEWARE_HEADER = 'x-middleware-set-cookie'
5+
6+
Deno.test('mergeMiddlewareCookies', async (t) => {
7+
await t.step('should handle empty cookies', async () => {
8+
const request = new Request('https://www.test-url.com')
9+
const response = new Response()
10+
11+
const result = mergeMiddlewareCookies(response, request)
12+
assertEquals(result, '')
13+
})
14+
15+
await t.step('should return request cookies when there are no middleware headers', async () => {
16+
const request = new Request('https://www.test-url.com')
17+
const response = new Response()
18+
19+
request.headers.set('Cookie', 'oatmeal=raisin')
20+
21+
const result = mergeMiddlewareCookies(response, request)
22+
assertEquals(result, 'oatmeal=raisin')
23+
})
24+
25+
await t.step('should not require cookies in request to be set', async () => {
26+
const request = new Request('https://www.test-url.com')
27+
const response = new Response()
28+
29+
response.headers.set(MIDDLEWARE_HEADER, 'peanut=butter; Path=/')
30+
31+
const result = mergeMiddlewareCookies(response, request)
32+
assertEquals(result, 'peanut=butter')
33+
})
34+
35+
await t.step('should merge request and middleware cookies', async () => {
36+
const request = new Request('https://www.test-url.com')
37+
const response = new Response()
38+
39+
request.headers.set('Cookie', 'oatmeal=raisin')
40+
response.headers.set(MIDDLEWARE_HEADER, 'peanut=butter; Path=/')
41+
42+
const result = mergeMiddlewareCookies(response, request)
43+
assertEquals(result, 'oatmeal=raisin; peanut=butter')
44+
})
45+
46+
await t.step('should overwrite request cookies with latest values', async () => {
47+
const request = new Request('https://www.test-url.com')
48+
const response = new Response()
49+
50+
request.headers.set('Cookie', 'oatmeal=chocolate')
51+
response.headers.set(MIDDLEWARE_HEADER, 'oatmeal=raisin; Path=/')
52+
53+
const result = mergeMiddlewareCookies(response, request)
54+
assertEquals(result, 'oatmeal=raisin')
55+
})
56+
57+
await t.step('should not decode middleware cookie values', async () => {
58+
const request = new Request('https://www.test-url.com')
59+
const response = new Response()
60+
61+
response.headers.set(MIDDLEWARE_HEADER, 'greeting=Hello%20from%20the%20cookie; Path=/')
62+
63+
const result = mergeMiddlewareCookies(response, request)
64+
assertEquals(result, 'greeting=Hello%20from%20the%20cookie')
65+
})
66+
67+
await t.step('should support multiple cookies being set in middleware', async () => {
68+
const request = new Request('https://www.test-url.com')
69+
const response = new Response()
70+
71+
response.headers.set(
72+
MIDDLEWARE_HEADER,
73+
'oatmeal=raisin; Path=/,peanut=butter; Path=/,chocolate=chip; Path=/',
74+
)
75+
76+
const result = mergeMiddlewareCookies(response, request)
77+
assertEquals(result, 'oatmeal=raisin; peanut=butter; chocolate=chip')
78+
})
79+
80+
await t.step('should ignore comma in middleware cookie expiry', async () => {
81+
const request = new Request('https://www.test-url.com')
82+
const response = new Response()
83+
84+
response.headers.set(
85+
MIDDLEWARE_HEADER,
86+
'oatmeal=raisin; Path=/; Expires=Wed, 23 Apr 2025 13:37:43 GMT; Max-Age=604800',
87+
)
88+
89+
const result = mergeMiddlewareCookies(response, request)
90+
assertEquals(result, 'oatmeal=raisin')
91+
})
92+
})

‎edge-runtime/lib/middleware.ts

Copy file name to clipboardExpand all lines: edge-runtime/lib/middleware.ts
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Context } from '@netlify/edge-functions'
22

33
import type { ElementHandlers } from '../vendor/deno.land/x/htmlrewriter@v1.0.0/src/index.ts'
4+
import { getCookies } from '../vendor/deno.land/std@0.175.0/http/cookie.ts'
45

56
type NextDataTransform = <T>(data: T) => T
67

@@ -58,3 +59,28 @@ export const addMiddlewareHeaders = async (
5859
})
5960
return response
6061
}
62+
63+
// This serves the same purpose as the mergeMiddlewareCookies in Next.js but has been customized to our domain
64+
// See: https://github.com/vercel/next.js/blob/6e4495f8430eab33b12cd11dffdd8e27eee6e0cf/packages/next/src/server/async-storage/request-store.ts#L78-L105
65+
export function mergeMiddlewareCookies(middlewareResponse: Response, lambdaRequest: Request) {
66+
let mergedCookies = getCookies(lambdaRequest.headers)
67+
const middlewareCookies = middlewareResponse.headers.get('x-middleware-set-cookie')
68+
69+
if (middlewareCookies) {
70+
// Targets commas that are not followed by whitespace
71+
// See: https://github.com/vercel/next.js/blob/e6145d3a37bb4c7b481fd58e05cdff9046ace8ad/packages/next/src/server/web/spec-extension/response.ts#L58-L66
72+
const regex = new RegExp(/,(?!\s)/)
73+
74+
middlewareCookies.split(regex).forEach((entry) => {
75+
// Extra directives within a cookie are joined on separated by "; "
76+
// See: https://github.com/vercel/next.js/blob/0edb1123066a010eff2aac274f948ca2c6e85c0f/packages/next/src/compiled/%40edge-runtime/cookies/index.js#L32-L47
77+
const [cookie] = entry.split('; ')
78+
const [name, value] = cookie.split('=')
79+
mergedCookies[name] = value
80+
})
81+
}
82+
83+
return Object.entries(mergedCookies)
84+
.map((kv) => kv.join('='))
85+
.join('; ')
86+
}

‎edge-runtime/lib/response.ts

Copy file name to clipboardExpand all lines: edge-runtime/lib/response.ts
+40-18Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {
66

77
import { updateModifiedHeaders } from './headers.ts'
88
import type { StructuredLogger } from './logging.ts'
9-
import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts'
9+
import {
10+
addMiddlewareHeaders,
11+
isMiddlewareRequest,
12+
isMiddlewareResponse,
13+
mergeMiddlewareCookies,
14+
} from './middleware.ts'
1015
import { RequestData } from './next-request.ts'
1116
import {
1217
addBasePath,
@@ -116,12 +121,13 @@ export const buildResponse = async ({
116121
}
117122
return rewriter.transform(response.originResponse)
118123
}
119-
const res = new Response(result.response.body, result.response)
124+
125+
const edgeResponse = new Response(result.response.body, result.response)
120126
request.headers.set('x-nf-next-middleware', 'skip')
121127

122-
let rewrite = res.headers.get('x-middleware-rewrite')
123-
let redirect = res.headers.get('location')
124-
let nextRedirect = res.headers.get('x-nextjs-redirect')
128+
let rewrite = edgeResponse.headers.get('x-middleware-rewrite')
129+
let redirect = edgeResponse.headers.get('location')
130+
let nextRedirect = edgeResponse.headers.get('x-nextjs-redirect')
125131

126132
// Data requests (i.e. requests for /_next/data ) need special handling
127133
const isDataReq = request.headers.has('x-nextjs-data')
@@ -152,7 +158,7 @@ export const buildResponse = async ({
152158
// Data requests might be rewritten to an external URL
153159
// This header tells the client router the redirect target, and if it's external then it will do a full navigation
154160

155-
res.headers.set('x-nextjs-rewrite', relativeUrl)
161+
edgeResponse.headers.set('x-nextjs-rewrite', relativeUrl)
156162
}
157163

158164
if (rewriteUrl.origin !== baseUrl.origin) {
@@ -178,7 +184,7 @@ export const buildResponse = async ({
178184
})
179185
}
180186

181-
return addMiddlewareHeaders(fetch(proxyRequest, { redirect: 'manual' }), res)
187+
return addMiddlewareHeaders(fetch(proxyRequest, { redirect: 'manual' }), edgeResponse)
182188
}
183189

184190
if (isDataReq) {
@@ -197,9 +203,17 @@ export const buildResponse = async ({
197203
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
198204
return
199205
}
200-
res.headers.set('x-middleware-rewrite', relativeUrl)
206+
edgeResponse.headers.set('x-middleware-rewrite', relativeUrl)
201207
request.headers.set('x-middleware-rewrite', target)
202-
return addMiddlewareHeaders(context.rewrite(target), res)
208+
209+
// coookies set in middleware need to be available during the lambda request
210+
const newRequest = new Request(target, request)
211+
const newRequestCookies = mergeMiddlewareCookies(edgeResponse, newRequest)
212+
if (newRequestCookies) {
213+
newRequest.headers.set('Cookie', newRequestCookies)
214+
}
215+
216+
return addMiddlewareHeaders(context.next(newRequest), edgeResponse)
203217
}
204218

205219
if (redirect) {
@@ -208,27 +222,35 @@ export const buildResponse = async ({
208222
logger.withFields({ redirect_url: redirect }).debug('Redirect url is same as original url')
209223
return
210224
}
211-
res.headers.set('location', redirect)
225+
edgeResponse.headers.set('location', redirect)
212226
}
213227

214228
// Data requests shouldn't automatically redirect in the browser (they might be HTML pages): they're handled by the router
215229
if (redirect && isDataReq) {
216-
res.headers.delete('location')
217-
res.headers.set('x-nextjs-redirect', relativizeURL(redirect, request.url))
230+
edgeResponse.headers.delete('location')
231+
edgeResponse.headers.set('x-nextjs-redirect', relativizeURL(redirect, request.url))
218232
}
219233

220-
nextRedirect = res.headers.get('x-nextjs-redirect')
234+
nextRedirect = edgeResponse.headers.get('x-nextjs-redirect')
221235

222236
if (nextRedirect && isDataReq) {
223-
res.headers.set('x-nextjs-redirect', normalizeDataUrl(nextRedirect))
237+
edgeResponse.headers.set('x-nextjs-redirect', normalizeDataUrl(nextRedirect))
224238
}
225239

226-
if (res.headers.get('x-middleware-next') === '1') {
227-
res.headers.delete('x-middleware-next')
228-
return addMiddlewareHeaders(context.next(), res)
240+
if (edgeResponse.headers.get('x-middleware-next') === '1') {
241+
edgeResponse.headers.delete('x-middleware-next')
242+
243+
// coookies set in middleware need to be available during the lambda request
244+
const newRequest = new Request(request)
245+
const newRequestCookies = mergeMiddlewareCookies(edgeResponse, newRequest)
246+
if (newRequestCookies) {
247+
newRequest.headers.set('Cookie', newRequestCookies)
248+
}
249+
250+
return addMiddlewareHeaders(context.next(newRequest), edgeResponse)
229251
}
230252

231-
return res
253+
return edgeResponse
232254
}
233255

234256
/**

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.