diff --git a/src/fetch.ts b/src/fetch.ts index b4029fe..3c2b440 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -6,14 +6,17 @@ import { isPayloadMethod, isJSONSerializable, detectResponseType, - mergeFetchOptions, + resolveFetchOptions, callHooks, } from "./utils"; import type { CreateFetchOptions, FetchResponse, + ResponseType, FetchContext, $Fetch, + FetchRequest, + FetchOptions, } from "./types"; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status @@ -88,13 +91,18 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { throw error; } - const $fetchRaw: $Fetch["raw"] = async function $fetchRaw( - _request, - _options = {} - ) { + const $fetchRaw: $Fetch["raw"] = async function $fetchRaw< + T = any, + R extends ResponseType = "json", + >(_request: FetchRequest, _options: FetchOptions = {}) { const context: FetchContext = { request: _request, - options: mergeFetchOptions(_options, globalOptions.defaults, Headers), + options: resolveFetchOptions( + _request, + _options, + globalOptions.defaults as unknown as FetchOptions, + Headers + ), response: undefined, error: undefined, }; @@ -110,11 +118,8 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { if (context.options.baseURL) { context.request = withBase(context.request, context.options.baseURL); } - if (context.options.query || context.options.params) { - context.request = withQuery(context.request, { - ...context.options.params, - ...context.options.query, - }); + if (context.options.query) { + context.request = withQuery(context.request, context.options.query); } } diff --git a/src/types.ts b/src/types.ts index e9c9cbd..5f2e818 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,11 +23,17 @@ export interface FetchOptions extends Omit, FetchHooks { baseURL?: string; + body?: RequestInit["body"] | Record; + ignoreResponseError?: boolean; + params?: Record; + query?: Record; + parseResponse?: (responseText: string) => any; + responseType?: R; /** @@ -61,6 +67,13 @@ export interface FetchOptions retryStatusCodes?: number[]; } +export interface ResolvedFetchOptions< + R extends ResponseType = ResponseType, + T = any, +> extends FetchOptions { + headers: Headers; +} + export interface CreateFetchOptions { defaults?: FetchOptions; fetch?: Fetch; @@ -79,7 +92,7 @@ export type GlobalOptions = Pick< export interface FetchContext { request: FetchRequest; - options: FetchOptions; + options: ResolvedFetchOptions; response?: FetchResponse; error?: Error; } diff --git a/src/utils.ts b/src/utils.ts index f9760ab..0efd414 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,8 @@ import type { FetchContext, FetchHook, FetchOptions, + FetchRequest, + ResolvedFetchOptions, ResponseType, } from "./types"; @@ -69,40 +71,59 @@ export function detectResponseType(_contentType = ""): ResponseType { return "blob"; } -// Merging of fetch option objects. -export function mergeFetchOptions( - input: FetchOptions | undefined, - defaults: FetchOptions | undefined, - Headers = globalThis.Headers -): FetchOptions { - const merged: FetchOptions = { - ...defaults, - ...input, - }; +export function resolveFetchOptions< + R extends ResponseType = ResponseType, + T = any, +>( + request: FetchRequest, + input: FetchOptions | undefined, + defaults: FetchOptions | undefined, + Headers: typeof globalThis.Headers +): ResolvedFetchOptions { + // Merge headers + const headers = mergeHeaders( + input?.headers ?? (request as Request)?.headers, + defaults?.headers, + Headers + ); - // Merge params and query - if (defaults?.params && input?.params) { - merged.params = { + // Merge query/params + let query: Record | undefined; + if (defaults?.query || defaults?.params || input?.params || input?.query) { + query = { ...defaults?.params, - ...input?.params, - }; - } - if (defaults?.query && input?.query) { - merged.query = { ...defaults?.query, + ...input?.params, ...input?.query, }; } - // Merge headers - if (defaults?.headers && input?.headers) { - merged.headers = new Headers(defaults?.headers || {}); - for (const [key, value] of new Headers(input?.headers || {})) { - merged.headers.set(key, value); + return { + ...defaults, + ...input, + query, + params: query, + headers, + }; +} + +function mergeHeaders( + input: HeadersInit | undefined, + defaults: HeadersInit | undefined, + Headers: typeof globalThis.Headers +): Headers { + if (!defaults) { + return new Headers(input); + } + const headers = new Headers(defaults); + if (input) { + for (const [key, value] of Symbol.iterator in input || Array.isArray(input) + ? input + : new Headers(input)) { + headers.set(key, value); } } - - return merged; + return headers; } export async function callHooks( diff --git a/test/index.test.ts b/test/index.test.ts index abbbd26..aabb9dd 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -383,7 +383,24 @@ describe("ofetch", () => { "x-header-c": "3", }); - expect(path).to.eq("?b=2&c=3&a=1"); + const parseParams = (str: string) => + Object.fromEntries(new URLSearchParams(str).entries()); + expect(parseParams(path)).toMatchObject(parseParams("?b=2&c=3&a=1")); + }); + + it("uses request headers", async () => { + expect( + await $fetch( + new Request(getURL("echo"), { headers: { foo: "1" } }), + {} + ).then((r) => r.headers) + ).toMatchObject({ foo: "1" }); + + expect( + await $fetch(new Request(getURL("echo"), { headers: { foo: "1" } }), { + headers: { foo: "2", bar: "3" }, + }).then((r) => r.headers) + ).toMatchObject({ foo: "2", bar: "3" }); }); it("calls hooks", async () => {