Skip to content

Commit d9be23f

Browse files
forbergFryuni
authored andcommitted
feat(request-state): add content-length header when opting out of stream (#270)
1 parent 77b0b6b commit d9be23f

File tree

3 files changed

+44
-11
lines changed

3 files changed

+44
-11
lines changed

.changeset/tiny-cars-sort.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@inox-tools/request-state': patch
3+
---
4+
5+
Added calculation of the `Content-Length` header when opting out of streaming with an injected state.
6+
This ensures responses with an injected state include the correct `Content-Length` header.
7+
Unmodified responses (like `HEAD` or `304 Not Modified`) continue to work exactly as before, avoiding overwriting the existing `Content-Length` header with an empty size.

packages/request-state/e2e/basic.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,17 @@ test('can set state to undefined', async ({ page }) => {
9393
const regularResult = await page.locator('pre#regular-result').innerHTML();
9494
expect(regularResult).toBe('hello');
9595
});
96+
97+
test('has content-length header', async ({ request }) => {
98+
server = await fixture.preview({});
99+
100+
const pageUrl = fixture.resolveUrl('/?name=John+Doe');
101+
102+
const response = await request.get(pageUrl);
103+
console.log('STATUS:', response.status());
104+
console.log('HEADERS:', response.headers());
105+
expect(response.status()).toBe(200);
106+
const contentLength = response.headers()['content-length'];
107+
expect(contentLength).toBeDefined();
108+
expect(Number(contentLength)).toBeGreaterThan(0);
109+
});

packages/request-state/src/runtime/middleware.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { defineMiddleware } from 'astro/middleware';
22
import { collectState } from './serverState.js';
33
import { parse } from 'content-type';
44

5+
const encoder = new TextEncoder();
6+
57
export const onRequest = defineMiddleware(async (_, next) => {
68
const { getState, result } = await collectState(next);
79

@@ -19,17 +21,27 @@ export const onRequest = defineMiddleware(async (_, next) => {
1921
const stateScript = state
2022
? `<script class="it-astro-state" type="application/json+devalue">${state}</script>`
2123
: null;
22-
if (stateScript) {
23-
const headCloseIndex = originalBody.indexOf('</head>');
24-
if (headCloseIndex > -1) {
25-
return new Response(
26-
originalBody.slice(0, headCloseIndex) + stateScript + originalBody.slice(headCloseIndex),
27-
result
28-
);
29-
} else {
30-
return new Response(stateScript + originalBody, result);
31-
}
24+
25+
if (!stateScript) {
26+
return new Response(originalBody, result);
3227
}
3328

34-
return new Response(originalBody, result);
29+
const headCloseIndex = originalBody.indexOf('</head>');
30+
const finalBody =
31+
headCloseIndex > -1
32+
? originalBody.slice(0, headCloseIndex) + stateScript + originalBody.slice(headCloseIndex)
33+
: stateScript + originalBody;
34+
35+
// TextEncoder is also supported in CloudFlare Workers, so this should work in all environments Astro supports.
36+
const encodedBody = encoder.encode(finalBody);
37+
const contentLength = encodedBody.byteLength;
38+
39+
const headers = new Headers(result.headers);
40+
headers.set('Content-Length', contentLength.toString());
41+
42+
return new Response(encodedBody, {
43+
status: result.status,
44+
statusText: result.statusText,
45+
headers,
46+
});
3547
});

0 commit comments

Comments
 (0)