Skip to content

Commit 61886b9

Browse files
committed
feat(transform): add re-calculataion of content-length header
1 parent 75cfd69 commit 61886b9

File tree

5 files changed

+113
-4
lines changed

5 files changed

+113
-4
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ compressible.
148148
For example, image media such as `image/jpag` is not compressible because it is
149149
already compressed.
150150

151+
## Content-Length
152+
153+
If the Response has a `Content-Length` header, compression may cause the actual
154+
content length to deviate.
155+
156+
In that case, the `Content-Length` is recalculated.
157+
158+
```ts
159+
import { compression } from "https://deno.land/x/compression_middleware@$VERSION/mod.ts";
160+
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
161+
162+
declare const request: Request;
163+
const middleware = compression();
164+
165+
const response = await middleware(
166+
request,
167+
() =>
168+
new Response("<body>", { headers: { "content-length": "<body:length>" } }),
169+
);
170+
171+
assertEquals(await response.text(), "<gzip:body>");
172+
assertEquals(response.headers.get("content-length"), "<gzip:body:length>");
173+
```
174+
151175
## Effects
152176

153177
Middleware may make changes to the following elements of the HTTP message.
@@ -156,6 +180,7 @@ Middleware may make changes to the following elements of the HTTP message.
156180
- HTTP Headers
157181
- Content-Encoding
158182
- Vary
183+
- [Content-Length](#content-length)
159184

160185
## Conditions
161186

transform.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
parseMediaType,
99
RepresentationHeader,
1010
} from "./deps.ts";
11-
import { isNoTransform } from "./utils.ts";
11+
import { isNoTransform, reCalcContentLength } from "./utils.ts";
1212
import type { Encoder } from "./types.ts";
1313

1414
/** Response with `Content-Encoding` header. */
@@ -33,7 +33,9 @@ export async function withContentEncoding(
3333
if (!isCompressible) return response;
3434

3535
const bodyInit = await context.encode(response.body);
36-
const newResponse = new Response(bodyInit, response);
36+
const newResponse = await reCalcContentLength(
37+
new Response(bodyInit, response),
38+
);
3739

3840
newResponse.headers.set(
3941
RepresentationHeader.ContentEncoding,

transform_test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ describe("withContentEncoding", () => {
2727
);
2828
});
2929

30+
it("should re-calculate content length if the response has content-length header", async () => {
31+
const encode = spy(() => "abc");
32+
33+
const initResponse = new Response("abcdef", {
34+
headers: { "content-length": "6" },
35+
});
36+
37+
const response = await withContentEncoding(initResponse, {
38+
encoding: "xxx",
39+
encode,
40+
});
41+
42+
assertSpyCalls(encode, 1);
43+
assert(
44+
await equalsResponse(
45+
response,
46+
new Response("abc", {
47+
headers: { "content-length": "3", "content-encoding": "xxx" },
48+
}),
49+
true,
50+
),
51+
);
52+
});
53+
3054
it("should not apply if the response does not have content-type header", async () => {
3155
const encode = spy(() => "abc");
3256

utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
1+
import { RepresentationHeader } from "./deps.ts";
2+
13
const ReNoTransform = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
24

35
export function isNoTransform(input: string): boolean {
46
return ReNoTransform.test(input);
57
}
8+
9+
export async function reCalcContentLength(
10+
response: Response,
11+
): Promise<Response> {
12+
if (
13+
response.bodyUsed ||
14+
!response.headers.has(RepresentationHeader.ContentLength)
15+
) return response;
16+
17+
const length = await response
18+
.clone()
19+
.arrayBuffer()
20+
.then((buffer) => buffer.byteLength)
21+
.then(String);
22+
23+
response.headers.set(RepresentationHeader.ContentLength, length);
24+
25+
return response;
26+
}

utils_test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { isNoTransform } from "./utils.ts";
2-
import { assert, describe, it } from "./_dev_deps.ts";
1+
import { isNoTransform, reCalcContentLength } from "./utils.ts";
2+
import { assert, describe, equalsResponse, it } from "./_dev_deps.ts";
33

44
describe("isNoTransform", () => {
55
it("should return true", () => {
@@ -28,3 +28,40 @@ describe("isNoTransform", () => {
2828
});
2929
});
3030
});
31+
32+
describe("reCalcContentLength", () => {
33+
it("should have new content-length if the response has content-length header", async () => {
34+
const response = await reCalcContentLength(
35+
new Response("abc", { headers: { "content-length": "100000" } }),
36+
);
37+
38+
assert(
39+
equalsResponse(
40+
response,
41+
new Response("abc", { headers: { "content-length": "3" } }),
42+
true,
43+
),
44+
);
45+
});
46+
47+
it("should return same response if the response has been read", async () => {
48+
const initResponse = new Response("abc", {
49+
headers: { "content-length": "100000" },
50+
});
51+
52+
await initResponse.text();
53+
54+
assert(initResponse.bodyUsed);
55+
56+
const response = await reCalcContentLength(initResponse);
57+
58+
assert(initResponse === response);
59+
});
60+
61+
it("should return same response if the response does not have content-length header", async () => {
62+
const initResponse = new Response("abc");
63+
const response = await reCalcContentLength(initResponse);
64+
65+
assert(initResponse === response);
66+
});
67+
});

0 commit comments

Comments
 (0)