Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: always clone and normalize options.headers and options.query #436

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<R> = {}) {
const context: FetchContext = {
request: _request,
options: mergeFetchOptions(_options, globalOptions.defaults, Headers),
options: resolveFetchOptions<R, T>(
_request,
_options,
globalOptions.defaults as unknown as FetchOptions<R, T>,
Headers
),
response: undefined,
error: undefined,
};
Expand All @@ -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);
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ export interface FetchOptions<R extends ResponseType = ResponseType, T = any>
extends Omit<RequestInit, "body">,
FetchHooks<T, R> {
baseURL?: string;

body?: RequestInit["body"] | Record<string, any>;

ignoreResponseError?: boolean;

params?: Record<string, any>;

query?: Record<string, any>;

parseResponse?: (responseText: string) => any;

responseType?: R;

/**
Expand Down Expand Up @@ -61,6 +67,13 @@ export interface FetchOptions<R extends ResponseType = ResponseType, T = any>
retryStatusCodes?: number[];
}

export interface ResolvedFetchOptions<
R extends ResponseType = ResponseType,
T = any,
> extends FetchOptions<R, T> {
headers: Headers;
}

export interface CreateFetchOptions {
defaults?: FetchOptions;
fetch?: Fetch;
Expand All @@ -79,7 +92,7 @@ export type GlobalOptions = Pick<

export interface FetchContext<T = any, R extends ResponseType = ResponseType> {
request: FetchRequest;
options: FetchOptions<R>;
options: ResolvedFetchOptions<R>;
response?: FetchResponse<T>;
error?: Error;
}
Expand Down
71 changes: 46 additions & 25 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {
FetchContext,
FetchHook,
FetchOptions,
FetchRequest,
ResolvedFetchOptions,
ResponseType,
} from "./types";

Expand Down Expand Up @@ -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<R, T> | undefined,
defaults: FetchOptions<R, T> | undefined,
Headers: typeof globalThis.Headers
): ResolvedFetchOptions<R, T> {
// 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<string, any> | 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<C extends FetchContext = FetchContext>(
Expand Down
19 changes: 18 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading