Skip to content

Commit ae26d18

Browse files
authored
Handle GET and HEAD request bodies during rewrites (#17813)
1 parent 6a46994 commit ae26d18

4 files changed

Lines changed: 111 additions & 16 deletions

File tree

.changeset/tiny-mirrors-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes `rewrite()` and `next(payload)` for GET and HEAD requests with host-provided bodies

packages/astro/src/core/middleware/sequence.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AstroError } from '../errors/index.js';
99
// module, which sits on the other side of the middleware import cycle.
1010
import type { FetchState } from '../fetch/fetch-state.js';
1111
import { getParams } from '../render/params-and-props.js';
12-
import { setOriginPathname } from '../routing/rewrite.js';
12+
import { copyRequest, setOriginPathname } from '../routing/rewrite.js';
1313
import { defineMiddleware } from './defineMiddleware.js';
1414

1515
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
@@ -40,19 +40,6 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
4040
const result = handle(handleContext, async (payload?: RewritePayload) => {
4141
if (i < length - 1) {
4242
if (payload) {
43-
let newRequest;
44-
if (payload instanceof Request) {
45-
newRequest = payload;
46-
} else if (payload instanceof URL) {
47-
// Cloning the original request ensures that the new Request gets its own copy of the body stream
48-
// Without this it will throw an error if they both try to consume the stream, which will happen in a rewrite
49-
newRequest = new Request(payload, handleContext.request.clone());
50-
} else {
51-
newRequest = new Request(
52-
new URL(payload, handleContext.url.origin),
53-
handleContext.request.clone(),
54-
);
55-
}
5643
const oldPathname = handleContext.url.pathname;
5744
const state = Reflect.get(handleContext, fetchStateSymbol) as FetchState | undefined;
5845
if (!state) {
@@ -67,6 +54,18 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
6754
payload,
6855
handleContext.request,
6956
);
57+
let newRequest: Request;
58+
if (payload instanceof Request) {
59+
newRequest = payload;
60+
} else {
61+
const request =
62+
handleContext.request.method === 'GET' || handleContext.request.method === 'HEAD'
63+
? handleContext.request
64+
: handleContext.request.clone();
65+
const newUrl =
66+
payload instanceof URL ? payload : new URL(payload, handleContext.url.origin);
67+
newRequest = copyRequest(newUrl, request, false, state.logger, routeData.route);
68+
}
7069

7170
// This is a case where the user tries to rewrite from a SSR route to a prerendered route (SSG).
7271
// This case isn't valid because when building for SSR, the prerendered route disappears from the server output because it becomes an HTML file,

packages/astro/src/core/routing/rewrite.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,14 @@ export function copyRequest(
149149
logger: AstroLogger,
150150
routePattern: string,
151151
): Request {
152-
if (oldRequest.bodyUsed) {
152+
const canHaveBody = oldRequest.method !== 'GET' && oldRequest.method !== 'HEAD';
153+
if (canHaveBody && oldRequest.bodyUsed) {
153154
throw new AstroError(AstroErrorData.RewriteWithBodyUsed);
154155
}
155156
return createRequest({
156157
url: newUrl,
157158
method: oldRequest.method,
158-
body: oldRequest.body,
159+
body: canHaveBody ? oldRequest.body : undefined,
159160
isPrerendered,
160161
logger,
161162
headers: isPrerendered ? {} : oldRequest.headers,

packages/astro/test/units/routing/rewrite-app.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ const postBPage = createComponent(async (result: any, props: any, slots: any) =>
3131
return render`<h1>Post B</h1><h2>${email}</h2>`;
3232
});
3333

34+
function createRequestWithHostProvidedBody(method: 'GET' | 'HEAD', url: string) {
35+
const request = new Request(url, {
36+
method: 'POST',
37+
body: 'x',
38+
headers: { 'content-length': '1', 'content-type': 'text/plain' },
39+
});
40+
Object.defineProperty(request, 'method', { value: method });
41+
return request;
42+
}
43+
3444
describe('Rewrites via App - basic', () => {
3545
const app = createTestApp([
3646
createPage(rewriteTo('/'), { route: '/reroute' }),
@@ -125,6 +135,86 @@ describe('Rewrites via App - POST body forwarding', () => {
125135
});
126136
});
127137

138+
describe('Rewrites via App - GET and HEAD body handling', () => {
139+
const rewrittenRequests: Request[] = [];
140+
const targetPage = createComponent((result: any, props: any, slots: any) => {
141+
const Astro = result.createAstro(props, slots);
142+
rewrittenRequests.push(Astro.request);
143+
return render`<h1>Target</h1>`;
144+
});
145+
const app = createTestApp([
146+
createPage(rewriteTo('/target'), { route: '/source' }),
147+
createPage(targetPage, { route: '/target' }),
148+
]);
149+
150+
for (const method of ['GET', 'HEAD'] as const) {
151+
it(`omits a host-provided ${method} body from the rewritten request`, async () => {
152+
rewrittenRequests.length = 0;
153+
const res = await app.render(
154+
createRequestWithHostProvidedBody(method, 'http://example.com/source'),
155+
);
156+
157+
assert.equal(res.status, 200);
158+
const rewrittenRequest = rewrittenRequests.at(-1);
159+
assert.ok(rewrittenRequest);
160+
assert.equal(rewrittenRequest.method, method);
161+
assert.equal(rewrittenRequest.body, null);
162+
});
163+
}
164+
});
165+
166+
describe('Rewrites via App - GET and HEAD body handling in middleware sequences', () => {
167+
const sequencedRequests: Request[] = [];
168+
const first = async (_context: APIContext, next: MiddlewareNext) => next('/post/post-b');
169+
const second = async (context: APIContext, next: MiddlewareNext) => {
170+
sequencedRequests.push(context.request);
171+
return next();
172+
};
173+
const app = createTestApp(
174+
[
175+
createPage(
176+
createComponent(() => render``),
177+
{ route: '/source' },
178+
),
179+
createPage(postBPage, { route: '/post/post-b' }),
180+
],
181+
{ middleware: () => ({ onRequest: sequence(first, second) }) },
182+
);
183+
184+
for (const method of ['GET', 'HEAD'] as const) {
185+
it(`omits a host-provided ${method} body before running the next middleware`, async () => {
186+
sequencedRequests.length = 0;
187+
const res = await app.render(
188+
createRequestWithHostProvidedBody(method, 'http://example.com/source'),
189+
);
190+
191+
assert.equal(res.status, 200);
192+
const sequencedRequest = sequencedRequests.at(-1);
193+
assert.ok(sequencedRequest);
194+
assert.equal(sequencedRequest.method, method);
195+
assert.equal(sequencedRequest.body, null);
196+
});
197+
}
198+
199+
it('preserves a POST body through the sequenced and final rewritten requests', async () => {
200+
sequencedRequests.length = 0;
201+
const res = await app.render(
202+
new Request('http://example.com/source', {
203+
method: 'POST',
204+
body: JSON.stringify({ email: 'example@example.com' }),
205+
headers: { 'content-type': 'application/json' },
206+
}),
207+
);
208+
209+
assert.equal(res.status, 200);
210+
const $ = cheerio.load(await res.text());
211+
assert.match($('h2').text(), /example@example.com/);
212+
const sequencedRequest = sequencedRequests.at(-1);
213+
assert.ok(sequencedRequest);
214+
assert.deepEqual(await sequencedRequest.json(), { email: 'example@example.com' });
215+
});
216+
});
217+
128218
describe('Rewrites via App - URL and Request payloads', () => {
129219
const app = createTestApp([
130220
createPage(

0 commit comments

Comments
 (0)