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

Commit d0ba6fe

Browse files
committed
Make Request/Response getters enumerable
1 parent 8dad2ac commit d0ba6fe

File tree

2 files changed

+94
-35
lines changed

2 files changed

+94
-35
lines changed

packages/core/src/standards/http.ts

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// noinspection SuspiciousTypeOfGuard
2+
13
import assert from "assert";
24
import { Blob } from "buffer";
35
import http from "http";
@@ -38,6 +40,18 @@ import {
3840
import fetchSymbols from "undici/lib/fetch/symbols.js";
3941
import { IncomingRequestCfProperties, RequestInitCfProperties } from "./cf";
4042

43+
const inspect = Symbol.for("nodejs.util.inspect.custom");
44+
const nonEnumerable = Object.create(null);
45+
nonEnumerable.enumerable = false;
46+
47+
function makeEnumerable<T>(prototype: any, instance: T, keys: (keyof T)[]) {
48+
for (const key of keys) {
49+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key)!;
50+
descriptor.enumerable = true;
51+
Object.defineProperty(instance, key, descriptor);
52+
}
53+
}
54+
4155
export class Headers extends BaseHeaders {
4256
getAll(key: string): string[] {
4357
if (key.toLowerCase() !== "set-cookie") {
@@ -72,6 +86,7 @@ export const kInner = Symbol("kInner");
7286
const kInputGated = Symbol("kInputGated");
7387
const kFormDataFiles = Symbol("kFormDataFiles");
7488

89+
const enumerableBodyKeys: (keyof Body<any>)[] = ["body", "bodyUsed", "headers"];
7590
export class Body<Inner extends BaseRequest | BaseResponse> {
7691
[kInner]: Inner;
7792
[kInputGated] = false;
@@ -81,6 +96,23 @@ export class Body<Inner extends BaseRequest | BaseResponse> {
8196

8297
constructor(inner: Inner) {
8398
this[kInner] = inner;
99+
100+
makeEnumerable(Body.prototype, this, enumerableBodyKeys);
101+
Object.defineProperty(this, kInner, nonEnumerable);
102+
Object.defineProperty(this, kInputGated, nonEnumerable);
103+
Object.defineProperty(this, kFormDataFiles, nonEnumerable);
104+
}
105+
106+
[inspect](): Inner {
107+
return this[kInner];
108+
}
109+
110+
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);
84116
}
85117

86118
get body(): ReadableStream | null {
@@ -191,14 +223,6 @@ export class Body<Inner extends BaseRequest | BaseResponse> {
191223
this[kInputGated] && (await waitForOpenInputGate());
192224
return body;
193225
}
194-
195-
get headers(): Headers {
196-
if (this.#headers) return this.#headers;
197-
const headers = new Headers(this[kInner].headers);
198-
// @ts-expect-error internal kGuard isn't included in type definitions
199-
headers[fetchSymbols.kGuard] = this[kInner].headers[fetchSymbols.kGuard];
200-
return (this.#headers = headers);
201-
}
202226
}
203227

204228
export function withInputGating<Inner extends Body<BaseRequest | BaseResponse>>(
@@ -221,23 +245,30 @@ export interface RequestInit extends BaseRequestInit {
221245
readonly cf?: IncomingRequestCfProperties | RequestInitCfProperties;
222246
}
223247

248+
const enumerableRequestKeys: (keyof Request)[] = [
249+
"cf",
250+
"signal",
251+
"redirect",
252+
"url",
253+
"method",
254+
];
224255
export class Request extends Body<BaseRequest> {
225256
// noinspection TypeScriptFieldCanBeMadeReadonly
226257
#cf?: IncomingRequestCfProperties | RequestInitCfProperties;
227258

228259
constructor(input: RequestInfo, init?: RequestInit) {
229-
// noinspection SuspiciousTypeOfGuard
230260
const cf = input instanceof Request ? input.#cf : init?.cf;
231261
if (input instanceof BaseRequest && !init) {
232262
// For cloning
233263
super(input);
234264
} else {
235265
// Don't pass our strange hybrid Request to undici
236-
// noinspection SuspiciousTypeOfGuard
237266
if (input instanceof Request) input = input[kInner];
238267
super(new BaseRequest(input, init));
239268
}
240269
this.#cf = cf ? nonCircularClone(cf) : undefined;
270+
271+
makeEnumerable(Request.prototype, this, enumerableRequestKeys);
241272
}
242273

243274
clone(): Request {
@@ -301,6 +332,15 @@ export interface ResponseInit extends BaseResponseInit {
301332

302333
const kWaitUntil = Symbol("kWaitUntil");
303334

335+
const enumerableResponseKeys: (keyof Response)[] = [
336+
"webSocket",
337+
"url",
338+
"redirected",
339+
"ok",
340+
"statusText",
341+
"status",
342+
"type",
343+
];
304344
export class Response<
305345
WaitUntil extends any[] = unknown[]
306346
> extends Body<BaseResponse> {
@@ -346,6 +386,9 @@ export class Response<
346386
}
347387
this.#status = status;
348388
this.#webSocket = webSocket;
389+
390+
makeEnumerable(Response.prototype, this, enumerableResponseKeys);
391+
Object.defineProperty(this, kWaitUntil, nonEnumerable);
349392
}
350393

351394
clone(): Response {
@@ -414,7 +457,6 @@ export async function fetch(
414457
// https://developers.cloudflare.com/workers/examples/cache-using-fetch
415458

416459
// Don't pass our strange hybrid Request to undici
417-
// noinspection SuspiciousTypeOfGuard
418460
if (input instanceof Request) input = input[kInner];
419461
await waitForOpenOutputGate();
420462
const baseRes = await baseFetch(input, init);
@@ -423,24 +465,13 @@ export async function fetch(
423465
return withInputGating(res);
424466
}
425467

426-
const requestInitKeys: (keyof BaseRequestInit)[] = [
427-
"method",
428-
"keepalive",
429-
"headers",
430-
"body",
431-
"redirect",
432-
"integrity",
433-
"signal",
434-
];
435-
436468
export function createCompatFetch(
437469
compat: Compatibility,
438470
inner: typeof fetch = fetch
439471
): typeof fetch {
440472
const refusesUnknown = compat.isEnabled("fetch_refuses_unknown_protocols");
441473
const formDataFiles = compat.isEnabled("formdata_parser_supports_files");
442474
return async (input, init) => {
443-
// noinspection SuspiciousTypeOfGuard
444475
const url = new URL(
445476
input instanceof Request || input instanceof BaseRequest
446477
? input.url
@@ -455,19 +486,13 @@ export function createCompatFetch(
455486
if (refusesUnknown) {
456487
throw new TypeError(`Fetch API cannot load: ${url.toString()}`);
457488
} else {
458-
// Undici doesn't let you pass a Request as the init prop, without
459-
// losing the headers, so if we need to rewrite the URL, we have to
460-
// manually build a RequestInit dict so we don't lose those properties.
461-
// noinspection SuspiciousTypeOfGuard
462-
if (input instanceof Request) input = input[kInner];
463-
if (input instanceof BaseRequest) {
464-
const newInit: RequestInit = {};
465-
for (const key of requestInitKeys) {
466-
const value = init?.[key] ?? input[key];
467-
// @ts-expect-error RequestInit is defined as read-only
468-
if (value) newInit[key] = value;
469-
}
470-
init = newInit;
489+
if (init) {
490+
init = new Request(input, init);
491+
} else if (input instanceof BaseRequest) {
492+
// BaseRequest's properties aren't enumerable, so convert to a Request
493+
init = new Request(input);
494+
} else if (input instanceof Request) {
495+
init = input;
471496
}
472497
// Free to mutate this as we created url at the start of the function
473498
url.protocol = "http:";

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,23 @@ test("Request: clone retains form data file parsing option", async (t) => {
372372
resFormData = await clone.formData();
373373
t.is(resFormData.get("file"), "test");
374374
});
375+
test("Request: Object.keys() returns getters", async (t) => {
376+
const res = new Request("http://localhost", {
377+
headers: { "X-Key": "value " },
378+
});
379+
const keys = Object.keys(res);
380+
const expectedKeys = [
381+
"body",
382+
"bodyUsed",
383+
"headers",
384+
"cf",
385+
"signal",
386+
"redirect",
387+
"url",
388+
"method",
389+
];
390+
t.deepEqual(keys.sort(), expectedKeys.sort());
391+
});
375392

376393
test("withImmutableHeaders: makes Request's headers immutable", (t) => {
377394
const req = new Request("http://localhost");
@@ -509,6 +526,23 @@ test("Response: clone retains form data file parsing option", async (t) => {
509526
resFormData = await clone.formData();
510527
t.is(resFormData.get("file"), "test");
511528
});
529+
test("Response: Object.keys() returns getters", async (t) => {
530+
const res = new Response("body", { headers: { "X-Key": "value " } });
531+
const keys = Object.keys(res);
532+
const expectedKeys = [
533+
"body",
534+
"bodyUsed",
535+
"headers",
536+
"webSocket",
537+
"url",
538+
"redirected",
539+
"ok",
540+
"statusText",
541+
"status",
542+
"type",
543+
];
544+
t.deepEqual(keys.sort(), expectedKeys.sort());
545+
});
512546

513547
test("withWaitUntil: adds wait until to (Base)Response", async (t) => {
514548
const waitUntil = [1];

0 commit comments

Comments
 (0)