v4: the API's JSON and the page are sent compressed — a page of the feed went out as it was, twenty kilobytes where a fifth does: only the built frontend's files were compressed, at build time, and Deno's server leaves alone a response whose length Oak has set. compressMiddleware (api/middleware/compress.ts), installed after the error middleware and around everything that sets a body, sends an object or array, and a string whose type its route set, in brotli or gzip as the request's Accept-Encoding takes, a coding refused with q=0 not being taken, from a kilobyte up, the threshold the build compresses from. Brotli is at quality 5, the frontend's files, compressed once, staying at 11. Files, streams and bytes, a response already encoded, a HEAD and one marked no-transform go out as they are, and a compressible answer says Vary: Accept-Encoding whether compressed or not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
90
api/test/middleware/compress.test.ts
Normal file
90
api/test/middleware/compress.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { BASE_URL } from "../helpers.ts";
|
||||
import { assertEquals } from "@std/assert";
|
||||
import { Application } from "@oak/oak";
|
||||
import { brotliDecompressSync, gunzipSync } from "node:zlib";
|
||||
import { compressMiddleware } from "../../middleware/compress.ts";
|
||||
|
||||
const BIG = { success: true, data: { items: Array(200).fill("dump title") } };
|
||||
|
||||
function makeApp(): Application {
|
||||
const app = new Application();
|
||||
app.use(compressMiddleware);
|
||||
app.use((ctx) => {
|
||||
const path = ctx.request.url.pathname;
|
||||
if (path === "/big") ctx.response.body = BIG;
|
||||
if (path === "/small") ctx.response.body = { success: true };
|
||||
if (path === "/html") {
|
||||
ctx.response.type = "text/html";
|
||||
ctx.response.body = `<!doctype html>${"<p>gerbeur</p>".repeat(200)}`;
|
||||
}
|
||||
if (path === "/bytes") ctx.response.body = new Uint8Array(4096);
|
||||
if (path === "/encoded") {
|
||||
ctx.response.headers.set("Content-Encoding", "br");
|
||||
ctx.response.type = "text/css";
|
||||
ctx.response.body = "x".repeat(4096);
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
async function get(path: string, acceptEncoding?: string) {
|
||||
const res = await makeApp().handle(
|
||||
new Request(`${BASE_URL}${path}`, {
|
||||
headers: acceptEncoding ? { "Accept-Encoding": acceptEncoding } : {},
|
||||
}),
|
||||
);
|
||||
if (!res) throw new Error("no response");
|
||||
return { res, bytes: new Uint8Array(await res.arrayBuffer()) };
|
||||
}
|
||||
|
||||
const text = (bytes: Uint8Array) => new TextDecoder().decode(bytes);
|
||||
|
||||
Deno.test("compressMiddleware: JSON goes out in brotli, or gzip for a client without it", async () => {
|
||||
const br = await get("/big", "gzip, deflate, br");
|
||||
assertEquals(br.res.headers.get("Content-Encoding"), "br");
|
||||
assertEquals(
|
||||
br.res.headers.get("Content-Type"),
|
||||
"application/json; charset=UTF-8",
|
||||
);
|
||||
assertEquals(br.res.headers.get("Vary"), "Accept-Encoding");
|
||||
assertEquals(JSON.parse(text(brotliDecompressSync(br.bytes))), BIG);
|
||||
|
||||
const gzip = await get("/big", "gzip");
|
||||
assertEquals(gzip.res.headers.get("Content-Encoding"), "gzip");
|
||||
assertEquals(JSON.parse(text(gunzipSync(gzip.bytes))), BIG);
|
||||
|
||||
const refused = await get("/big", "br;q=0, gzip;q=0.5");
|
||||
assertEquals(refused.res.headers.get("Content-Encoding"), "gzip");
|
||||
});
|
||||
|
||||
Deno.test("compressMiddleware: a client that accepts no coding gets the JSON as it is", async () => {
|
||||
for (const accept of [undefined, "identity", "br;q=0"]) {
|
||||
const { res, bytes } = await get("/big", accept);
|
||||
assertEquals(res.headers.get("Content-Encoding"), null);
|
||||
// A cache still has to tell this answer from the compressed one.
|
||||
assertEquals(res.headers.get("Vary"), "Accept-Encoding");
|
||||
assertEquals(JSON.parse(text(bytes)), BIG);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("compressMiddleware: the HTML page is compressed too", async () => {
|
||||
const { res, bytes } = await get("/html", "br");
|
||||
assertEquals(res.headers.get("Content-Encoding"), "br");
|
||||
assertEquals(res.headers.get("Content-Type"), "text/html; charset=UTF-8");
|
||||
assertEquals(text(brotliDecompressSync(bytes)).startsWith("<!doctype"), true);
|
||||
});
|
||||
|
||||
Deno.test("compressMiddleware: short answers, bytes and encoded files are left alone", async () => {
|
||||
const small = await get("/small", "br");
|
||||
assertEquals(small.res.headers.get("Content-Encoding"), null);
|
||||
assertEquals(small.res.headers.get("Vary"), null);
|
||||
assertEquals(JSON.parse(text(small.bytes)), { success: true });
|
||||
|
||||
const bytes = await get("/bytes", "br");
|
||||
assertEquals(bytes.res.headers.get("Content-Encoding"), null);
|
||||
assertEquals(bytes.bytes.length, 4096);
|
||||
|
||||
const encoded = await get("/encoded", "br");
|
||||
assertEquals(encoded.res.headers.get("Content-Encoding"), "br");
|
||||
assertEquals(text(encoded.bytes), "x".repeat(4096));
|
||||
});
|
||||
Reference in New Issue
Block a user