Skip to content

feat(http/unstable): accept HeadersInit for serveDir and serveFile headers#7190

Open
minato32 wants to merge 5 commits into
denoland:mainfrom
minato32:feat/6723-headersinit
Open

feat(http/unstable): accept HeadersInit for serveDir and serveFile headers#7190
minato32 wants to merge 5 commits into
denoland:mainfrom
minato32:feat/6723-headersinit

Conversation

@minato32

Copy link
Copy Markdown
Contributor

Closes #6723.

ServeDirOptions.headers and ServeFileOptions.headers in @std/http/unstable-file-server now accept HeadersInit instead of string[], so callers can pass a Headers instance, a record, or a list of tuples without hand-rolling name:value strings.

The stable serveDir/serveFile keep their existing string[] signature, so the file-server CLI behaviour is unchanged. The unstable wrapper strips headers before delegating to the stable implementation and applies them to the response itself (skipping redirect responses, matching the stable behaviour).

@CLAassistant

CLAassistant commented Jun 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.84%. Comparing base (094f127) to head (f50174e).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
http/unstable_file_server.ts 96.15% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7190      +/-   ##
==========================================
+ Coverage   94.63%   94.84%   +0.20%     
==========================================
  Files         629      617      -12     
  Lines       51891    51690     -201     
  Branches     9373     9357      -16     
==========================================
- Hits        49105    49023      -82     
+ Misses       2217     2121      -96     
+ Partials      569      546      -23     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice ergonomic improvement — accepting HeadersInit is much friendlier than hand-built "name:value" strings, and the implementation reads cleanly. Verified the redirect-skip matches stable serveDir and that headers is correctly stripped before delegating so there's no double-application. Coverage is good (record + Headers instance + redirect skip, for both serveDir/serveFile).

A couple of non-blocking notes inline. Also worth calling out in the changelog: this is a hard break for the unstable API (old headers: ["X-Extra: extra header"] callers now fail to type-check and throw at runtime via new Headers(...)). That's fine for unstable — just be explicit about it. LGTM overall.

function appendHeaders(target: Headers, headers: HeadersInit): void {
const normalized = new Headers(headers);
for (const [name, value] of normalized) {
target.append(name, value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Using .append() means custom headers are added alongside any the file server already set, rather than replacing them — e.g. passing cache-control would yield a comma-joined value rather than overriding. This matches the previous string[] behavior so it's not a regression, but now that the API takes a rich HeadersInit, callers may reasonably expect to override (e.g. set their own Cache-Control). Worth a one-line doc note on the headers option clarifying that values are appended, not replaced. (The cache-control test only passes because the dir-listing response doesn't pre-set that header.)

Comment thread http/file_server_test.ts Outdated
const req = new Request("http://localhost/hello");
const res = await unstableServeDir(req, {
...serveDirOptions,
...serveDirOptions as UnstableServeDirOptions,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The as UnstableServeDirOptions cast (repeated at 5 call sites) is forced by Omit<StableServeDirOptions, "headers"> + redefine: the spread carries stable's headers?: string[], which isn't assignable to HeadersInit. You could avoid all the casts by typing the shared serveDirOptions fixture as UnstableServeDirOptions (or using satisfies). Minor, test-only.

Document that serveDir/serveFile headers are appended rather than
replacing pre-set headers, and type the test fixture with satisfies so
the per-call UnstableServeDirOptions casts are no longer needed.

Addresses @bartlomieju's review feedback.
@minato32

Copy link
Copy Markdown
Contributor Author

@bartlomieju updated in 6f87cb1 — added a doc note on both headers options clarifying values are appended (not replaced), and switched the test fixture to satisfies ServeDirOptions so the per-call casts are gone. Thanks for the review!

@bartlomieju

Copy link
Copy Markdown
Member

Could we accept both forms here rather than swapping string[] out for HeadersInit? Even though this is unstable, existing callers passing the legacy headers: ["X-Extra: extra header"] (a flat string[] of "name: value" strings) would start failing both at type-check time and at runtime — new Headers(["X-Extra: extra header"]) throws because the entries aren't [name, value] pairs.

Suggestion: type it as HeadersInit | string[] and branch in appendHeaders — if it's a flat array of plain strings, parse the legacy "name: value" form; otherwise hand it to new Headers(). Something like:

function appendHeaders(target: Headers, headers: HeadersInit | string[]): void {
  // Legacy form: a flat array of "name: value" strings.
  if (Array.isArray(headers) && typeof headers[0] === "string") {
    for (const header of headers as string[]) {
      const i = header.indexOf(":");
      target.append(header.slice(0, i), header.slice(i + 1).trimStart());
    }
    return;
  }
  for (const [name, value] of new Headers(headers)) {
    target.append(name, value);
  }
}

That keeps the new HeadersInit ergonomics while leaving the old ["name: value"] usage working. A test covering the legacy string[] input would be good to lock it in.

…serveFile

Type `headers` as `HeadersInit | string[]` and branch in appendHeaders
to parse the legacy `"name: value"` string array form, so existing
callers keep working alongside the new HeadersInit ergonomics.

Addresses @bartlomieju's review feedback.
@minato32

Copy link
Copy Markdown
Contributor Author

@bartlomieju updated in f50174eheaders is now typed HeadersInit | string[] and appendHeaders branches on the legacy flat "name: value" string array, so old headers: ["X-Extra: extra header"] callers keep working. Added legacy-string[] tests for both serveDir and serveFile, and noted the accepted forms in the option docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

suggestion(http): change ServeDirOptions.headers to accept HeadersInit instead of string[]

3 participants