Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 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,14 @@ 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)
}
},
...(request.body ? { body: request.body, duplex: 'half' } : {}),
});
Comment on lines 129 to 138
Copy link

Copilot AI Mar 31, 2026

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
27 changes: 27 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,33 @@ describe('Vercel edge middleware', () => {
assert.ok((await response.text()).length, 'Body is included');
});

it('edge middleware forwards HTTP method and body', async () => {
const entry = new URL(
'../.vercel/output/functions/_middleware.func/middleware.mjs',
build.config.outDir,
);
const module = await import(entry);

const originalFetch = globalThis.fetch;
let captured;
globalThis.fetch = async (_url, opts) => {
captured = opts;
return new Response('ok', { status: 200 });
};
try {
const request = new Request('http://example.com/api/test', {
method: 'POST',
body: '{"data":"test"}',
headers: { 'Content-Type': 'application/json' },
});
await module.default(request, {});
assert.equal(captured.method, 'POST', 'forwards the HTTP method');
assert.ok(captured.body, 'forwards the request body');
} finally {
globalThis.fetch = originalFetch;
}
});

// 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