Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit ca7a9f8

Browse files
committed
Add Headers#getAll by prototype manipulation, closes #73
1 parent 58c22f4 commit ca7a9f8

File tree

5 files changed

+29
-35
lines changed

5 files changed

+29
-35
lines changed

packages/core/src/plugins/core.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ import {
3333
SetupResult,
3434
globsToMatcher,
3535
} from "@miniflare/shared";
36-
import { File, FormData } from "undici";
36+
import { File, FormData, Headers } from "undici";
3737
import {
3838
DOMException,
3939
FetchEvent,
40-
Headers,
4140
Request,
4241
Response,
4342
ScheduledEvent,

packages/core/src/standards/http.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type { BusboyHeaders } from "busboy";
1818
import { Colorize, blue, bold, green, grey, red, yellow } from "kleur/colors";
1919
import { splitCookiesString } from "set-cookie-parser";
2020
import {
21-
Headers as BaseHeaders,
2221
Request as BaseRequest,
2322
RequestInfo as BaseRequestInfo,
2423
RequestInit as BaseRequestInit,
@@ -27,6 +26,7 @@ import {
2726
BodyInit,
2827
File,
2928
FormData,
29+
Headers,
3030
RequestCache,
3131
RequestCredentials,
3232
RequestDestination,
@@ -52,17 +52,24 @@ function makeEnumerable<T>(prototype: any, instance: T, keys: (keyof T)[]) {
5252
}
5353
}
5454

55-
export class Headers extends BaseHeaders {
56-
getAll(key: string): string[] {
57-
if (key.toLowerCase() !== "set-cookie") {
58-
throw new TypeError(
59-
'getAll() can only be used with the header name "Set-Cookie".'
60-
);
61-
}
62-
const value = super.get("set-cookie");
63-
return value ? splitCookiesString(value) : [];
55+
// Manipulating the prototype like this isn't very nice. However, this is a
56+
// non-standard function so it's unlikely to cause problems with other people's
57+
// code. Miniflare is also usually the only thing running in a process.
58+
// The alternative would probably be to subclass Headers. However, we'd have
59+
// to construct a version of our Headers object from undici Headers, which
60+
// would copy the headers. If we then attempted to create a new Response from
61+
// this mutated-header Response, the headers wouldn't be copied, as we unwrap
62+
// our hybrid Response before passing it to undici.
63+
// @ts-expect-error getAll is non-standard
64+
Headers.prototype.getAll = function (key: string): string[] {
65+
if (key.toLowerCase() !== "set-cookie") {
66+
throw new TypeError(
67+
'getAll() can only be used with the header name "Set-Cookie".'
68+
);
6469
}
65-
}
70+
const value = this.get("set-cookie");
71+
return value ? splitCookiesString(value) : [];
72+
};
6673

6774
// Instead of subclassing our customised Request and Response classes from
6875
// BaseRequest and BaseResponse, we instead compose them and implement the same
@@ -92,7 +99,6 @@ export class Body<Inner extends BaseRequest | BaseResponse> {
9299
[kInputGated] = false;
93100
[kFormDataFiles] = true; // Default to enabling form-data File parsing
94101
#inputGatedBody?: ReadableStream;
95-
#headers?: Headers;
96102

97103
constructor(inner: Inner) {
98104
this[kInner] = inner;
@@ -108,11 +114,7 @@ export class Body<Inner extends BaseRequest | BaseResponse> {
108114
}
109115

110116
get headers(): Headers {
111-
if (this.#headers) return this.#headers;
112-
const headers = new Headers(this[kInner].headers);
113-
// @ts-expect-error internal kGuard isn't included in type definitions
114-
headers[fetchSymbols.kGuard] = this[kInner].headers[fetchSymbols.kGuard];
115-
return (this.#headers = headers);
117+
return this[kInner].headers;
116118
}
117119

118120
get body(): ReadableStream | null {

packages/core/src/standards/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export * from "./domexception";
44
export * from "./encoding";
55
export * from "./event";
66
export {
7-
Headers,
87
Body,
98
withInputGating,
109
withStringFormDataFiles,

packages/core/test/standards/http.spec.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { text } from "stream/consumers";
33
import { ReadableStream, TransformStream, WritableStream } from "stream/web";
44
import {
55
Body,
6-
Headers,
76
IncomingRequestCfProperties,
87
Request,
98
Response,
@@ -26,26 +25,30 @@ import {
2625
import { WebSocketPair } from "@miniflare/web-sockets";
2726
import test, { Macro } from "ava";
2827
import {
29-
Headers as BaseHeaders,
3028
Request as BaseRequest,
3129
Response as BaseResponse,
3230
BodyMixin,
3331
File,
3432
FormData,
33+
Headers,
3534
} from "undici";
3635

3736
// @ts-expect-error filling out all properties is annoying
3837
const cf: IncomingRequestCfProperties = { country: "GB" };
3938

4039
test('Headers: getAll: throws if key not "Set-Cookie"', (t) => {
4140
const headers = new Headers();
41+
// @ts-expect-error getAll is added to the Headers prototype by importing
42+
// @miniflare/core
4243
t.throws(() => headers.getAll("set-biscuit"), {
4344
instanceOf: TypeError,
4445
message: 'getAll() can only be used with the header name "Set-Cookie".',
4546
});
4647
});
4748
test("Headers: getAll: returns empty array if no headers", (t) => {
4849
const headers = new Headers();
50+
// @ts-expect-error getAll is added to the Headers prototype by importing
51+
// @miniflare/core
4952
t.deepEqual(headers.getAll("Set-Cookie"), []);
5053
});
5154
test("Headers: getAll: returns separated Set-Cookie values", (t) => {
@@ -57,6 +60,8 @@ test("Headers: getAll: returns separated Set-Cookie values", (t) => {
5760
headers.append("Set-Cookie", cookie2);
5861
headers.append("Set-Cookie", cookie3);
5962
t.is(headers.get("set-Cookie"), [cookie1, cookie2, cookie3].join(", "));
63+
// @ts-expect-error getAll is added to the Headers prototype by importing
64+
// @miniflare/core
6065
t.deepEqual(headers.getAll("set-CoOkiE"), [cookie1, cookie2, cookie3]);
6166
});
6267

@@ -148,19 +153,6 @@ test(
148153
);
149154
test(inputGatedConsumerMacro, "json");
150155
test(inputGatedConsumerMacro, "text");
151-
test("Body: reuses custom headers instance", (t) => {
152-
const headers = new BaseHeaders();
153-
headers.append("Set-Cookie", "key1=value1");
154-
headers.append("Set-Cookie", "key2=value2");
155-
const body = new Body(new BaseResponse("body", { headers }));
156-
const customHeaders = body.headers;
157-
t.not(headers, customHeaders);
158-
t.is(body.headers, customHeaders);
159-
t.deepEqual(customHeaders.getAll("Set-Cookie"), [
160-
"key1=value1",
161-
"key2=value2",
162-
]);
163-
});
164156
test("Body: formData: parses regular form data fields", async (t) => {
165157
// Check with application/x-www-form-urlencoded Content-Type
166158
let body = new Body(

packages/http-server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
171171
key = key.toLowerCase();
172172
if (key === "set-cookie") {
173173
// Multiple Set-Cookie headers should be treated as separate headers
174+
// @ts-expect-error getAll is added to the Headers prototype by
175+
// importing @miniflare/core
174176
headers["set-cookie"] = response.headers.getAll("set-cookie");
175177
} else {
176178
headers[key] = value;

0 commit comments

Comments
 (0)