Skip to content
Open
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
17 changes: 14 additions & 3 deletions src/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { stat } from "node:fs/promises";
import { createReadStream } from "node:fs";
import { createGzip, createBrotliCompress } from "node:zlib";
import { createVFSHandler } from "./vfs.ts";
import type { CompressOptions } from "../types/config.ts";

import devErrorHandler, {
defaultHandler as devErrorHandlerInternal,
Expand Down Expand Up @@ -70,6 +71,7 @@ export class NitroDevApp {
dir: asset.dir,
base: assetBase,
fallthrough: asset.fallthrough,
compress: this.nitro.options.compressPublicAssets,
})
);
}
Expand Down Expand Up @@ -97,7 +99,7 @@ export class NitroDevApp {
// TODO: upstream to h3/node
function serveStaticDir(
event: H3Event,
opts: { dir: string; base: string; fallthrough?: boolean }
opts: { dir: string; base: string; fallthrough?: boolean; compress?: boolean | CompressOptions }
) {
const dir = resolve(opts.dir) + "/";
const r = (id: string) => {
Expand All @@ -107,6 +109,15 @@ function serveStaticDir(
return resolved;
}
};

// Determine compression settings
const compressOpts =
opts.compress === true
? { gzip: true, brotli: true, zstd: false }
: opts.compress === false
? { gzip: false, brotli: false, zstd: false }
: opts.compress || { gzip: false, brotli: false, zstd: false };

return serveStatic(event, {
fallthrough: opts.fallthrough,
getMeta: async (id) => {
Expand All @@ -126,12 +137,12 @@ function serveStaticDir(
if (!path) return;
const stream = createReadStream(path);
const acceptEncoding = event.req.headers.get("accept-encoding") || "";
if (acceptEncoding.includes("br")) {
if (compressOpts.brotli && acceptEncoding.includes("br")) {
event.res.headers.set("Content-Encoding", "br");
event.res.headers.delete("Content-Length");
event.res.headers.set("Vary", "Accept-Encoding");
return stream.pipe(createBrotliCompress());
} else if (acceptEncoding.includes("gzip")) {
} else if (compressOpts.gzip && acceptEncoding.includes("gzip")) {
Comment on lines +140 to +145
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor Accept-Encoding quality values (q) instead of substring matching.

Line 140 and Line 145 use .includes("br") / .includes("gzip"), which will incorrectly compress even when the client sends br;q=0 or gzip;q=0.

πŸ’‘ Proposed fix
-      const acceptEncoding = event.req.headers.get("accept-encoding") || "";
-      if (compressOpts.brotli && acceptEncoding.includes("br")) {
+      const acceptEncoding = event.req.headers.get("accept-encoding") || "";
+      if (compressOpts.brotli && acceptsEncoding(acceptEncoding, "br")) {
         event.res.headers.set("Content-Encoding", "br");
         event.res.headers.delete("Content-Length");
         event.res.headers.set("Vary", "Accept-Encoding");
         return stream.pipe(createBrotliCompress());
-      } else if (compressOpts.gzip && acceptEncoding.includes("gzip")) {
+      } else if (compressOpts.gzip && acceptsEncoding(acceptEncoding, "gzip")) {
         event.res.headers.set("Content-Encoding", "gzip");
         event.res.headers.delete("Content-Length");
         event.res.headers.set("Vary", "Accept-Encoding");
         return stream.pipe(createGzip());
       }
function acceptsEncoding(header: string, encoding: "br" | "gzip") {
  return header.split(",").some((item) => {
    const [name, ...params] = item.trim().toLowerCase().split(";");
    if (name !== encoding && name !== "*") {
      return false;
    }
    const q = params
      .map((p) => p.trim())
      .find((p) => p.startsWith("q="))
      ?.slice(2);

    if (!q) {
      return true;
    }

    const quality = Number.parseFloat(q);
    return Number.isFinite(quality) && quality > 0;
  });
}
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dev/app.ts` around lines 140 - 145, Replace the naive substring checks
for brotli/gzip in the compression branch with an Accept-Encoding parser that
honors q-values: add an acceptsEncoding(header, encoding) helper (as proposed)
and use it instead of acceptEncoding.includes("br") and
acceptEncoding.includes("gzip") in the logic around
stream.pipe(createBrotliCompress()) and the gzip branch; the helper should split
on commas, parse each item into name and params, treat "*" as matching, parse
any "q=" param to a number, and only return true if a matching encoding has no q
or q > 0.0 so clients that send q=0 are correctly respected.

event.res.headers.set("Content-Encoding", "gzip");
event.res.headers.delete("Content-Length");
event.res.headers.set("Vary", "Accept-Encoding");
Expand Down