Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-tigers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': patch
---

Fixes edge middleware `next()` dropping the HTTP method and body when forwarding requests to the serverless function, which caused non-GET API routes (POST, PUT, PATCH, DELETE) to return 404
5 changes: 4 additions & 1 deletion packages/integrations/vercel/src/serverless/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,15 @@ export default async function middleware(request, context) {
const next = async () => {
const { vercel, ...locals } = ctx.locals;
const response = await fetch(new URL('/${NODE_PATH}', request.url), {
method: request.method,
headers: {
...Object.fromEntries(request.headers.entries()),
'${ASTRO_MIDDLEWARE_SECRET_HEADER}': '${middlewareSecret}',
'${ASTRO_PATH_HEADER}': request.url.replace(origin, ''),
'${ASTRO_LOCALS_HEADER}': trySerializeLocals(locals)
}
},
body: request.body,
duplex: 'half',
Comment thread
Crystora marked this conversation as resolved.
Outdated
});
Comment on lines 129 to 138

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

body: request.body and duplex: 'half' are set unconditionally. In Fetch implementations based on undici/Node semantics, providing a body for GET/HEAD can throw at runtime (“Request with GET/HEAD method cannot have body”) and duplex is only meaningful when a streaming body is actually present. Consider conditionally including body (and duplex) only when the method allows a body and request.body is non-null (e.g., omit for GET/HEAD). This keeps forwarding safe and avoids sending unnecessary init fields.

Copilot uses AI. Check for mistakes.
return new Response(response.body, {
status: response.status,
Expand Down
12 changes: 12 additions & 0 deletions packages/integrations/vercel/test/edge-middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ describe('Vercel edge middleware', () => {
assert.ok((await response.text()).length, 'Body is included');
});

it('edge middleware forwards HTTP method and body', async () => {
const contents = await build.readFile(
'../.vercel/output/functions/_middleware.func/middleware.mjs',
);
assert.ok(contents.includes('method: request.method'), 'forwards the HTTP method');
assert.ok(contents.includes('body: request.body'), 'forwards the request body');
assert.ok(
contents.includes("duplex: 'half'") || contents.includes('duplex: "half"'),

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

These assertions are fairly formatting-sensitive (exact substrings, quote variants, spacing). This can make the test fail on benign codegen formatting changes. Consider using a single regex per assertion that tolerates whitespace/quote differences (and ideally scopes the match to the fetch( options block) so the test verifies the behavior without coupling tightly to formatting.

Suggested change
assert.ok(contents.includes('method: request.method'), 'forwards the HTTP method');
assert.ok(contents.includes('body: request.body'), 'forwards the request body');
assert.ok(
contents.includes("duplex: 'half'") || contents.includes('duplex: "half"'),
assert.match(
contents,
/fetch\([\s\S]*?{[\s\S]*?method\s*:\s*request\.method[\s\S]*?}/,
'forwards the HTTP method',
);
assert.match(
contents,
/fetch\([\s\S]*?{[\s\S]*?body\s*:\s*request\.body[\s\S]*?}/,
'forwards the request body',
);
assert.match(
contents,
/fetch\([\s\S]*?{[\s\S]*?duplex\s*:\s*['"]half['"][\s\S]*?}/,

Copilot uses AI. Check for mistakes.
'sets duplex to half for streaming body',
);
});

// TODO: The path here seems to be inconsistent?
it.skip('with edge handle file, should successfully build the middleware', async () => {
const fixture = await loadFixture({
Expand Down
Loading