diff --git a/src/browser.ts b/src/browser.ts index 39c6ded..e1ee3d4 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -66,44 +66,11 @@ function streamToArrayBuffer(stream: ReadableStream) { */ export class Body implements CommonBody { $rawBody: RawBody | null | typeof kBodyUsed | typeof kBodyDestroyed; - headers: Headers; - constructor(body: CreateBody, headers: Headers) { + constructor(body: CreateBody) { const rawBody = body === undefined ? null : body; this.$rawBody = rawBody; - this.headers = headers; - - if (rawBody === null) return; - - if (typeof rawBody === "string") { - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "text/plain"); - } - - if (!headers.has("Content-Length")) { - headers.set("Content-Length", byteLength(rawBody).toString()); - } - - return; - } - - // Default to "octet stream" for raw bodies. - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "application/octet-stream"); - } - - if (rawBody instanceof ArrayBuffer) { - if (!headers.has("Content-Length")) { - headers.set("Content-Length", rawBody.byteLength.toString()); - } - - return; - } - - if (rawBody instanceof ReadableStream) return; - - throw new TypeError("Unknown body type"); } get bodyUsed() { @@ -172,10 +139,10 @@ export class Body implements CommonBody { if (rawBody instanceof ReadableStream) { const [selfRawBody, clonedRawBody] = rawBody.tee(); this.$rawBody = selfRawBody; - return new Body(clonedRawBody, this.headers.clone()); + return new Body(clonedRawBody); } - return new Body(rawBody, this.headers.clone()); + return new Body(rawBody); } destroy(): Promise { @@ -194,25 +161,35 @@ export class Body implements CommonBody { export class Request extends Body implements CommonRequest { url: string; method: string; + headers: Headers; trailer: Promise; readonly signal: Signal; constructor(input: string | Request, init: RequestOptions = {}) { // Clone request or use passed options object. - const opts = typeof input === "string" ? init : input.clone(); - const headers = new Headers(init.headers || opts.headers); - const rawBody = - init.body || (opts instanceof Request ? getRawBody(opts) : null); - - super(rawBody, headers); + const req = typeof input === "string" ? undefined : input.clone(); + const rawBody = init.body || (req ? getRawBody(req) : null); + const headers = + req && !init.headers + ? req.headers + : getDefaultHeaders( + rawBody, + init.headers, + init.omitDefaultHeaders === true + ); + + super(rawBody); this.url = typeof input === "string" ? input : input.url; - this.method = init.method || opts.method || "GET"; - this.signal = init.signal || opts.signal || new Signal(); + this.method = init.method || (req && req.method) || "GET"; + this.signal = init.signal || (req && req.signal) || new Signal(); this.headers = headers; - this.trailer = Promise.resolve( - init.trailer || opts.trailer - ).then(x => new Headers(x)); + this.trailer = + req && !init.trailer + ? req.trailer + : Promise.resolve(init.trailer).then( + x => new Headers(x) + ); // Destroy body on abort. once(this.signal, "abort", () => this.destroy()); @@ -223,7 +200,8 @@ export class Request extends Body implements CommonRequest { return new Request(this.url, { body: getRawBody(cloned), - headers: cloned.headers, + headers: this.headers.clone(), + omitDefaultHeaders: true, method: this.method, signal: this.signal, trailer: this.trailer.then(x => x.clone()) @@ -237,21 +215,26 @@ export class Request extends Body implements CommonRequest { export class Response extends Body implements CommonResponse { status: number; statusText: string; + headers: Headers; trailer: Promise; get ok() { return this.status >= 200 && this.status < 300; } - constructor(body?: CreateBody, opts: ResponseOptions = {}) { - const headers = new Headers(opts.headers); + constructor(body?: CreateBody, init: ResponseOptions = {}) { + const headers = getDefaultHeaders( + body, + init.headers, + init.omitDefaultHeaders === true + ); - super(body, headers); + super(body); - this.status = opts.status || 200; - this.statusText = opts.statusText || ""; + this.status = init.status || 200; + this.statusText = init.statusText || ""; this.headers = headers; - this.trailer = Promise.resolve(opts.trailer).then( + this.trailer = Promise.resolve(init.trailer).then( x => new Headers(x) ); } @@ -262,8 +245,51 @@ export class Response extends Body implements CommonResponse { return new Response(getRawBody(cloned), { status: this.status, statusText: this.statusText, - headers: cloned.headers, + headers: this.headers.clone(), + omitDefaultHeaders: true, trailer: this.trailer.then(x => x.clone()) }); } } + +/** + * Get default headers for `Request` and `Response` instances. + */ +function getDefaultHeaders( + rawBody: CreateBody, + init: HeadersInit | undefined, + omitDefaultHeaders: boolean +) { + const headers = new Headers(init); + + if (rawBody === null || rawBody === undefined) return headers; + + if (typeof rawBody === "string") { + if (!omitDefaultHeaders && !headers.has("Content-Type")) { + headers.set("Content-Type", "text/plain"); + } + + if (!omitDefaultHeaders && !headers.has("Content-Length")) { + headers.set("Content-Length", byteLength(rawBody).toString()); + } + + return headers; + } + + // Default to "octet stream" for raw bodies. + if (!omitDefaultHeaders && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/octet-stream"); + } + + if (rawBody instanceof ArrayBuffer) { + if (!omitDefaultHeaders && !headers.has("Content-Length")) { + headers.set("Content-Length", rawBody.byteLength.toString()); + } + + return headers; + } + + if (rawBody instanceof ReadableStream) return headers; + + throw new TypeError("Unknown body type"); +} diff --git a/src/common.ts b/src/common.ts index cceed18..1c64425 100644 --- a/src/common.ts +++ b/src/common.ts @@ -48,7 +48,6 @@ export type CommonBodyConstructor = { */ export interface CommonBody { $rawBody: T | null | typeof kBodyUsed | typeof kBodyDestroyed; - headers: Headers; readonly bodyUsed: boolean; json(): Promise; text(): Promise; @@ -65,6 +64,7 @@ export interface CommonRequestOptions { body?: T; signal?: Signal; headers?: HeadersInit; + omitDefaultHeaders?: boolean; trailer?: HeadersInit | Promise; } @@ -74,6 +74,7 @@ export interface CommonRequestOptions { export interface CommonRequest extends CommonBody { url: string; method: string; + headers: Headers; trailer: Promise; readonly signal: Signal; clone(): CommonRequest; @@ -86,6 +87,7 @@ export interface CommonResponseOptions { status?: number; statusText?: string; headers?: HeadersInit; + omitDefaultHeaders?: boolean; trailer?: HeadersInit | Promise; } @@ -95,6 +97,7 @@ export interface CommonResponseOptions { export interface CommonResponse extends CommonBody { status: number; statusText: string; + headers: Headers; trailer: Promise; clone(): CommonResponse; } diff --git a/src/node.spec.ts b/src/node.spec.ts index 768275f..59c45e7 100644 --- a/src/node.spec.ts +++ b/src/node.spec.ts @@ -28,6 +28,54 @@ describe("node", () => { expect(req.headers.get("Test")).toEqual("1"); expect(req.headers.get("Other")).toEqual(null); }); + + it("should initialize default headers", () => { + const req = new Request("", { + body: "test" + }); + + expect(req.headers.get("Content-Type")).toEqual("text/plain"); + expect(req.headers.get("Content-Length")).toEqual("4"); + }); + + it("should skip default header initialization", () => { + const req = new Request("/", { + body: "test", + omitDefaultHeaders: true + }); + + expect(req.headers.get("Content-Length")).toEqual(null); + + const clonedReq = req.clone(); + + expect(clonedReq.headers.get("Content-Length")).toEqual(null); + + const initReq = new Request(req); + + expect(initReq.headers.get("Content-Length")).toEqual(null); + }); + + it("should clone new header instances", () => { + const req = new Request("/", { + headers: { + "Test": "true" + } + }) + + expect(req.headers.get("test")).toEqual("true"); + + const clonedReq = req.clone(); + clonedReq.headers.set("Test", "false"); + + expect(req.headers.get("test")).toEqual("true"); + expect(clonedReq.headers.get("test")).toEqual("false"); + + const initReq = new Request(req); + initReq.headers.set("Test", "false"); + + expect(req.headers.get("test")).toEqual("true"); + expect(initReq.headers.get("test")).toEqual("false"); + }) }); describe("body", () => { @@ -66,8 +114,8 @@ describe("node", () => { const fn = jest.fn(); - reqClone.signal.on('abort', fn); - req.signal.emit('abort'); + reqClone.signal.on("abort", fn); + req.signal.emit("abort"); expect(fn).toHaveBeenCalled(); }); diff --git a/src/node.ts b/src/node.ts index b3529ff..86d8a66 100644 --- a/src/node.ts +++ b/src/node.ts @@ -54,9 +54,8 @@ function streamToBuffer(stream: Readable): Promise { */ export class Body implements CommonBody { $rawBody: RawBody | null | typeof kBodyUsed | typeof kBodyDestroyed; - headers: Headers; - constructor(body: CreateBody, headers: Headers) { + constructor(body: CreateBody) { const rawBody = body === undefined ? null @@ -64,45 +63,7 @@ export class Body implements CommonBody { ? Buffer.from(body) : body; - this.headers = headers; this.$rawBody = rawBody; - - if (rawBody === null) return; - - if (typeof rawBody === "string") { - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "text/plain"); - } - - if (!headers.has("Content-Length")) { - headers.set("Content-Length", byteLength(rawBody).toString()); - } - - return; - } - - // Default to "octet stream" for raw bodies. - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "application/octet-stream"); - } - - if (isStream(rawBody)) { - if (typeof rawBody.getHeaders === "function") { - headers.extend(rawBody.getHeaders()); - } - - return; - } - - if (Buffer.isBuffer(rawBody)) { - if (!headers.has("Content-Length")) { - headers.set("Content-Length", String(rawBody.length)); - } - - return; - } - - throw new TypeError("Unknown body type"); } get bodyUsed() { @@ -167,10 +128,10 @@ export class Body implements CommonBody { if (isStream(rawBody)) { const clonedRawBody = rawBody.pipe(new PassThrough()); this.$rawBody = rawBody.pipe(new PassThrough()); - return new Body(clonedRawBody, this.headers.clone()); + return new Body(clonedRawBody); } - return new Body(rawBody, this.headers.clone()); + return new Body(rawBody); } destroy(): Promise { @@ -189,25 +150,35 @@ export class Body implements CommonBody { export class Request extends Body implements CommonRequest { url: string; method: string; + headers: Headers; trailer: Promise; readonly signal: Signal; constructor(input: string | Request, init: RequestOptions = {}) { // Clone request or use passed options object. - const opts = typeof input === "string" ? init : input.clone(); - const headers = new Headers(init.headers || opts.headers); - const rawBody = - init.body || (opts instanceof Request ? getRawBody(opts) : null); - - super(rawBody, headers); + const req = typeof input === "string" ? undefined : input.clone(); + const rawBody = init.body || (req ? getRawBody(req) : null); + const headers = + req && !init.headers + ? req.headers + : getDefaultHeaders( + rawBody, + init.headers, + init.omitDefaultHeaders === true + ); + + super(rawBody); this.url = typeof input === "string" ? input : input.url; - this.method = init.method || opts.method || "GET"; - this.signal = init.signal || opts.signal || new Signal(); + this.method = init.method || (req && req.method) || "GET"; + this.signal = init.signal || (req && req.signal) || new Signal(); this.headers = headers; - this.trailer = Promise.resolve( - init.trailer || opts.trailer - ).then(x => new Headers(x)); + this.trailer = + req && !init.trailer + ? req.trailer + : Promise.resolve(init.trailer).then( + x => new Headers(x) + ); // Destroy body on abort. once(this.signal, "abort", () => this.destroy()); @@ -218,7 +189,8 @@ export class Request extends Body implements CommonRequest { return new Request(this.url, { body: getRawBody(cloned), - headers: cloned.headers, + headers: this.headers.clone(), + omitDefaultHeaders: true, method: this.method, signal: this.signal, trailer: this.trailer.then(x => x.clone()) @@ -232,6 +204,7 @@ export class Request extends Body implements CommonRequest { export class Response extends Body implements CommonResponse { status: number; statusText: string; + headers: Headers; trailer: Promise; get ok() { @@ -239,9 +212,13 @@ export class Response extends Body implements CommonResponse { } constructor(body?: CreateBody, init: ResponseOptions = {}) { - const headers = new Headers(init.headers); + const headers = getDefaultHeaders( + body, + init.headers, + init.omitDefaultHeaders === true + ); - super(body, headers); + super(body); this.status = init.status || 200; this.statusText = init.statusText || ""; @@ -257,8 +234,51 @@ export class Response extends Body implements CommonResponse { return new Response(getRawBody(cloned), { status: this.status, statusText: this.statusText, - headers: cloned.headers, + headers: this.headers.clone(), + omitDefaultHeaders: true, trailer: this.trailer.then(x => x.clone()) }); } } + +/** + * Get default headers for `Request` and `Response` instances. + */ +function getDefaultHeaders( + rawBody: CreateBody, + init: HeadersInit | undefined, + omitDefaultHeaders: boolean +) { + const headers = new Headers(init); + + if (rawBody === null || rawBody === undefined) return headers; + + if (typeof rawBody === "string") { + if (!omitDefaultHeaders && !headers.has("Content-Type")) { + headers.set("Content-Type", "text/plain"); + } + + if (!omitDefaultHeaders && !headers.has("Content-Length")) { + headers.set("Content-Length", byteLength(rawBody).toString()); + } + + return headers; + } + + // Default to "octet stream" for raw bodies. + if (!omitDefaultHeaders && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/octet-stream"); + } + + if (rawBody instanceof ArrayBuffer) { + if (!omitDefaultHeaders && !headers.has("Content-Length")) { + headers.set("Content-Length", rawBody.byteLength.toString()); + } + + return headers; + } + + if (rawBody instanceof ReadableStream) return headers; + + throw new TypeError("Unknown body type"); +}