From 0c8c16a06cb3de10cc2603a3f9daf334fdef92bb Mon Sep 17 00:00:00 2001 From: Javad Mnjd Date: Wed, 27 Jul 2022 23:13:26 +0430 Subject: [PATCH] feat: add headers option to wrapFetch function --- CHANGELOG.md | 6 ++ README.md | 1 + extended_request_init.ts | 7 +- fetch_wrapper.ts | 65 ++++++++++--------- fetch_wrapper_test.ts | 42 ++++++++++++ header_utils.ts | 137 --------------------------------------- header_utils_test.ts | 111 ------------------------------- mod.ts | 1 - 8 files changed, 89 insertions(+), 281 deletions(-) delete mode 100644 header_utils.ts delete mode 100644 header_utils_test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7595598..c6d64f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## v5.1.0 + +- add support for initial headers when creating wrapper +- remove unnecessary header utils +- improve types for validator for easier header usage + ## v5.0.0 - fix timeout functionality as the abort signal is now supported as of deno v1.11 diff --git a/README.md b/README.md index 1967c0d..123eddb 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This library offers a fetch wrapper that can: - set a validator globally or per request, to reject when validator throws. - accept a timeout option and abort when timeout is reached - add `Accept` header with value `application/json, text/plain, */*` if not already set by you +- set global headers when creating the wrapper Version v5.0.0+ is the recommended version now (abort controller is used now). please don't use v4 of fetch goody anymore. diff --git a/extended_request_init.ts b/extended_request_init.ts index f0c06e7..509f897 100644 --- a/extended_request_init.ts +++ b/extended_request_init.ts @@ -16,9 +16,14 @@ interface RequestInitDiff { export type Validator = ( response: Response, - init: ExtendedRequestInit, + init: ExtendedRequest, ) => void | Promise; export type ExtendedRequestInit = & RequestInitDiff & Omit; + +export type ExtendedRequest = + & RequestInitDiff + & Omit + & { headers: Headers }; diff --git a/fetch_wrapper.ts b/fetch_wrapper.ts index 8b655a3..81461d1 100644 --- a/fetch_wrapper.ts +++ b/fetch_wrapper.ts @@ -1,6 +1,9 @@ import * as utils from "./utils.ts"; -import { getHeader, setHeader } from "./header_utils.ts"; -import { ExtendedRequestInit, Validator } from "./extended_request_init.ts"; +import { + ExtendedRequest, + ExtendedRequestInit, + Validator, +} from "./extended_request_init.ts"; /** * Transforms data and adds corresponding headers if possible. @@ -17,11 +20,7 @@ function transformData( | ReadableStream | null | undefined, - headers: - | Headers - | string[][] - | Record - | undefined, + headers: Headers, init?: RequestInit | ExtendedRequestInit, ): | string @@ -47,30 +46,21 @@ function transformData( return data.buffer; } if (utils.isURLSearchParams(data)) { - setHeader( - headers, + headers.set( "Content-Type", "application/x-www-form-urlencoded;charset=utf-8", ); return data.toString(); } if (utils.isObject(data)) { - setHeader( - headers, - "Content-Type", - "application/json;charset=utf-8", - ); + headers.set("Content-Type", "application/json;charset=utf-8"); if (init && !init.method) { init.method = "POST"; } return JSON.stringify(data); } // the default header if type undefined - setHeader( - headers, - "Content-Type", - "application/x-www-form-urlencoded", - ); + headers.set("Content-Type", "application/x-www-form-urlencoded"); return data; } @@ -83,6 +73,8 @@ export type WrapFetchOptions = { validator?: Validator; /** if set, all requests will timeout after this amount of milliseconds passed */ timeout?: number; + /** if set, will be used as default headers. new added headers will be added on top of these. */ + headers?: Headers; }; export function wrapFetch(options?: WrapFetchOptions) { @@ -91,6 +83,7 @@ export function wrapFetch(options?: WrapFetchOptions) { userAgent, validator, timeout = 99999999, + headers, } = options || {}; return async function wrappedFetch( @@ -103,25 +96,32 @@ export function wrapFetch(options?: WrapFetchOptions) { } const interceptedInit = init || {}; - if (!interceptedInit.headers) { - interceptedInit.headers = new Headers(); + + if (!(interceptedInit.headers instanceof Headers)) { + interceptedInit.headers = new Headers(interceptedInit.headers); + } + + { + const baseHeaders = new Headers(headers); + + for (const header of interceptedInit.headers) { + baseHeaders.set(header[0], header[1]); + } + + interceptedInit.headers = baseHeaders; } // setup a default accept - if (!getHeader(interceptedInit.headers, "Accept")) { - setHeader( - interceptedInit.headers, + if (!interceptedInit.headers.get("Accept")) { + interceptedInit.headers.set( "Accept", "application/json, text/plain, */*", ); } + // setup user agent if set if (userAgent) { - setHeader( - interceptedInit.headers, - "User-Agent", - userAgent, - ); + interceptedInit.headers.set("User-Agent", userAgent); } if ("form" in interceptedInit && interceptedInit.form) { @@ -215,14 +215,17 @@ export function wrapFetch(options?: WrapFetchOptions) { clearTimeout(timeoutId); if (typeof validator === "function") { - await validator(response, interceptedInit); + await validator(response, interceptedInit as ExtendedRequest); } if ( "validator" in interceptedInit && typeof interceptedInit.validator === "function" ) { - await interceptedInit.validator(response, interceptedInit); + await interceptedInit.validator( + response, + interceptedInit as ExtendedRequest, + ); } return response; diff --git a/fetch_wrapper_test.ts b/fetch_wrapper_test.ts index a668149..2193513 100644 --- a/fetch_wrapper_test.ts +++ b/fetch_wrapper_test.ts @@ -239,6 +239,48 @@ Deno.test("interaction with a server", { }, ); + await t.step( + "WrappedFetch sends the given headers when creating wrapper", + async () => { + const headers = new Headers(); + headers.set("test", "foo"); + headers.set("bar", "baz"); + const wrappedFetch = wrapFetch({ + headers, + }); + + let headerString = await wrappedFetch(serverOneUrl + "/test").then( + (r) => r.text(), + ); + assertStrictEquals(headerString, "foo"); + + headerString = await wrappedFetch(serverOneUrl + "/bar").then( + (r) => r.text(), + ); + assertStrictEquals(headerString, "baz"); + }, + ); + + await t.step( + "WrappedFetch doesnt override the user's newly given headers", + async () => { + const headers = new Headers(); + headers.set("test", "foo"); + const wrappedFetch = wrapFetch({ + headers, + }); + + const headerString = await wrappedFetch(serverOneUrl + "/test", { + headers: { + "test": "baz", + }, + }).then( + (r) => r.text(), + ); + assertStrictEquals(headerString, "baz"); + }, + ); + await t.step( "WrappedFetch sends FormData when formData is defined", async () => { diff --git a/header_utils.ts b/header_utils.ts deleted file mode 100644 index 3948ced..0000000 --- a/header_utils.ts +++ /dev/null @@ -1,137 +0,0 @@ -type unknownHeadersType = - | Headers - | string[][] - | Record - | undefined; - -function isHeadersOfTypeHeaders( - headers: unknownHeadersType, -): headers is Headers { - if (headers && "append" in headers && typeof headers.append === "function") { - return true; - } - return false; -} - -function isHeadersOfTypeArray( - headers: unknownHeadersType, -): headers is string[][] { - if (!isHeadersOfTypeHeaders(headers) && Array.isArray(headers)) { - return true; - } - return false; -} - -function isHeadersOfTypeObject( - headers: unknownHeadersType, -): headers is Record { - if (!isHeadersOfTypeArray(headers) && typeof headers === "object") { - return true; - } - return false; -} - -export function getHeader( - headers: Headers | string[][] | Record | undefined, - name: string, -): string | null { - if (isHeadersOfTypeHeaders(headers)) { - return headers.get(name); - } else if (isHeadersOfTypeArray(headers)) { - for (const [hName, hValue] of headers) { - if (hName.toLowerCase() === name.toLowerCase()) { - return hValue; - } - } - return null; - } else if (isHeadersOfTypeObject(headers)) { - for (const hName of Object.keys(headers)) { - if (hName.toLowerCase() === name.toLowerCase()) { - return headers[hName]; - } - } - } - return null; -} - -export function setHeader( - headers: Headers | string[][] | Record | undefined, - /** Header name */ - name: string, - /** Header value */ - value: string, -) { - if (isHeadersOfTypeHeaders(headers)) { - headers.set(name, value); - } else if (isHeadersOfTypeArray(headers)) { - let found = false; - for (const [index, [hName]] of headers.entries()) { - if (hName.toLowerCase() === name.toLowerCase()) { - headers[index][0] = name; - headers[index][1] = value; - found = true; - break; - } - } - if (!found) { - headers.push([name, value]); - } - } else if (isHeadersOfTypeObject(headers)) { - let found = false; - for (const hName of Object.keys(headers)) { - if (hName.toLowerCase() === name.toLowerCase()) { - delete headers[hName]; - headers[name] = value; - found = true; - break; - } - } - if (!found) { - headers[name] = value; - } - } -} - -export function appendHeader( - headers: Headers | string[][] | Record | undefined, - /** Header name */ - name: string, - /** Header value */ - value: string, -) { - if (isHeadersOfTypeHeaders(headers)) { - headers.append(name, value); - } else { - let newValue = getHeader(headers, name) || ""; - if (!newValue) { - newValue += value; - } else { - newValue += ", " + value; - } - setHeader(headers, name, newValue); - } -} - -export function deleteHeader( - headers: Headers | string[][] | Record | undefined, - name: string, -) { - if (isHeadersOfTypeHeaders(headers)) { - return headers.delete(name); - } else if (isHeadersOfTypeArray(headers)) { - for (const [index, [hName]] of headers.entries()) { - if (hName.toLowerCase() === name.toLowerCase()) { - headers.splice(index, 1); - return; - } - } - return null; - } else if (isHeadersOfTypeObject(headers)) { - for (const hName of Object.keys(headers)) { - if (hName.toLowerCase() === name.toLowerCase()) { - delete headers[hName]; - return; - } - } - } -} diff --git a/header_utils_test.ts b/header_utils_test.ts deleted file mode 100644 index 356d9f3..0000000 --- a/header_utils_test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - assertNotStrictEquals, - assertStrictEquals, - equal, -} from "https://deno.land/std@0.98.0/testing/asserts.ts"; -import { - appendHeader, - deleteHeader, - getHeader, - setHeader, -} from "./header_utils.ts"; - -Deno.test("deleteHeader works if header is of type Headers", () => { - const headers = new Headers(); - headers.set("foo", "bar"); - deleteHeader(headers, "foo"); - assertStrictEquals(headers.get("foo"), null); -}); - -Deno.test("deleteHeader works if header is of type object", () => { - const headers = { - "foo": "bar", - }; - deleteHeader(headers, "foo"); - assertStrictEquals(headers.foo, undefined); - assertNotStrictEquals(headers.foo, "bar"); -}); - -Deno.test("deleteHeader works if header is of type array", () => { - const headers = [ - ["foo", "bar"], - ]; - - deleteHeader(headers, "foo"); - assertStrictEquals(headers.length, 0); -}); - -Deno.test("setHeader works if header is of type Headers", () => { - const headers = new Headers(); - setHeader(headers, "foo", "bar"); - - assertStrictEquals(headers.get("foo"), "bar"); -}); - -Deno.test("setHeader works if header is of type object", () => { - const headers: Record = {}; - setHeader(headers, "foo", "bar"); - assertStrictEquals(headers["foo"], "bar"); - setHeader(headers, "foo", "baz"); - assertStrictEquals(headers["foo"], "baz"); -}); - -Deno.test("setHeader works if header is of type Array", () => { - const headers: string[][] = []; - - setHeader(headers, "foo", "bar"); - assertStrictEquals(headers.length, 1); - equal(headers[0], ["foo", "bar"]); - - setHeader(headers, "foo", "baz"); - assertStrictEquals(headers.length, 1); - equal(headers[0], ["foo", "baz"]); -}); - -Deno.test("appendHeader works if header is of type Headers", () => { - const headers = new Headers(); - appendHeader(headers, "foo", "bar"); - - assertStrictEquals(headers.get("foo"), "bar"); -}); - -Deno.test("appendHeader works if header is of type object", () => { - const headers: Record = {}; - appendHeader(headers, "foo", "bar"); - assertStrictEquals(headers["foo"], "bar"); - appendHeader(headers, "foo", "baz"); - assertStrictEquals(headers["foo"], "bar, baz"); -}); - -Deno.test("appendHeader works if header is of type Array", () => { - const headers: string[][] = []; - appendHeader(headers, "foo", "bar"); - - assertStrictEquals(headers.length, 1); - equal(headers[0], ["foo", "bar"]); - appendHeader(headers, "foo", "baz"); - assertStrictEquals(headers.length, 1); - equal(headers[0], ["foo", "bar, baz"]); -}); - -Deno.test("getHeader works if header is of type Headers", () => { - const headers = new Headers(); - headers.set("foo", "bar"); - - assertStrictEquals(getHeader(headers, "foo"), "bar"); -}); - -Deno.test("getHeader works if header is of type object", () => { - const headers: Record = { - foo: "bar", - }; - assertStrictEquals(getHeader(headers, "foo"), "bar"); -}); - -Deno.test("getHeader works if header is of type Array", () => { - const headers: string[][] = [ - ["foo", "bar"], - ]; - - assertStrictEquals(getHeader(headers, "foo"), "bar"); -}); diff --git a/mod.ts b/mod.ts index 85f979b..e4e06e7 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,2 @@ export * from "./fetch_wrapper.ts"; -export * from "./header_utils.ts"; export type { ExtendedRequestInit } from "./extended_request_init.ts";