Skip to content
Open
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
6 changes: 5 additions & 1 deletion packages/better-fetch/src/create-fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ export const createFetch = <Option extends CreateFetchOption>(
const opts = {
...config,
...options,
plugins: [...(config?.plugins || []), applySchemaPlugin(config || {}), ...(options?.plugins || [])],
plugins: [
...(config?.plugins || []),
applySchemaPlugin(config || {}),
...(options?.plugins || []),
],
} as BetterFetchOption;

if (config?.catchAllError) {
Expand Down
2 changes: 1 addition & 1 deletion packages/better-fetch/src/create-fetch/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { StandardSchemaV1 } from "../standard-schema";
import type { BetterFetchPlugin } from "../plugins";
import type { StandardSchemaV1 } from "../standard-schema";
import type { Prettify, StringLiteralUnion } from "../type-utils";
import type { BetterFetchOption, BetterFetchResponse } from "../types";
import type { FetchSchema, Schema } from "./schema";
Expand Down
188 changes: 131 additions & 57 deletions packages/better-fetch/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { BetterFetchError } from "./error";
import { initializePlugins } from "./plugins";
import {
type ErrorContext,
type FetchHooks,
type RequestContext,
initializePlugins,
} from "./plugins";
import { createRetryStrategy } from "./retry";
import type { StandardSchemaV1 } from "./standard-schema";
import type { BetterFetchOption, BetterFetchResponse } from "./types";
Expand All @@ -16,6 +21,80 @@ import {
parseStandardSchema,
} from "./utils";

type ErrorHandlerParams = {
errorContext: ErrorContext & { responseText?: string };
hooks: {
onError: Array<FetchHooks["onError"]>;
onRetry: Array<FetchHooks["onRetry"]>;
};
options?: BetterFetchOption;
url: string;
fetchFn: typeof betterFetch;
cloneResponse?: boolean;
throwError?: unknown;
};

async function handleError({
errorContext,
hooks,
options,
url,
fetchFn,
cloneResponse,
throwError,
}: ErrorHandlerParams): Promise<{ data: null; error: unknown }> {
for (const onError of hooks.onError) {
if (onError) {
await onError({
...errorContext,
response:
cloneResponse && errorContext.response
? errorContext.response.clone()
: errorContext.response,
});
}
}

if (options?.retry) {
const retryStrategy = createRetryStrategy(options.retry);
const _retryAttempt = options.retryAttempt ?? 0;
if (
await retryStrategy.shouldAttemptRetry(
_retryAttempt,
errorContext.response ?? null,
)
) {
for (const onRetry of hooks.onRetry) {
if (onRetry && errorContext.response) {
await onRetry({
response: errorContext.response,
request: errorContext.request,
});
}
}
const delay = retryStrategy.getDelay(_retryAttempt);
await new Promise((resolve) => setTimeout(resolve, delay));
return await fetchFn(url, {
...options,
retryAttempt: _retryAttempt + 1,
});
}
}

if (options?.throw) {
throw new BetterFetchError(
errorContext.error.status,
errorContext.error.statusText,
throwError ?? errorContext.error,
);
}

return {
data: null,
error: errorContext.error,
};
}

export const betterFetch = async <
TRes extends Option["output"] extends StandardSchemaV1
? StandardSchemaV1.InferOutput<Option["output"]>
Expand Down Expand Up @@ -73,7 +152,40 @@ export const betterFetch = async <
}

const { clearTimeout } = getTimeout(opts, controller);
let response = await fetch(context.url, context);

let response: Response;
try {
response = await fetch(context.url, context);
} catch (fetchError) {
clearTimeout();

const isAbortError =
fetchError instanceof DOMException && fetchError.name === "AbortError";
if (isAbortError) {
throw fetchError;
}

const networkError = {
status: 0,
statusText: "Network Error",
message:
fetchError instanceof Error ? fetchError.message : String(fetchError),
cause: fetchError,
};

return handleError({
errorContext: {
response: undefined,
request: context,
error: networkError,
},
hooks,
options,
url,
fetchFn: betterFetch,
}) as any;
}

clearTimeout();

const responseContext = {
Expand Down Expand Up @@ -158,61 +270,23 @@ export const betterFetch = async <
const responseText = await response.text();
const isJSONResponse = isJSONParsable(responseText);
const errorObject = isJSONResponse ? await parser(responseText) : null;
/**
* Error Branch
*/
const errorContext = {
response,
responseText,
request: context,
error: {
...errorObject,
status: response.status,
statusText: response.statusText,
},
};
for (const onError of hooks.onError) {
if (onError) {
await onError({
...errorContext,
response: options?.hookOptions?.cloneResponse
? response.clone()
: response,
});
}
}

if (options?.retry) {
const retryStrategy = createRetryStrategy(options.retry);
const _retryAttempt = options.retryAttempt ?? 0;
if (await retryStrategy.shouldAttemptRetry(_retryAttempt, response)) {
for (const onRetry of hooks.onRetry) {
if (onRetry) {
await onRetry(responseContext);
}
}
const delay = retryStrategy.getDelay(_retryAttempt);
await new Promise((resolve) => setTimeout(resolve, delay));
return await betterFetch(url, {
...options,
retryAttempt: _retryAttempt + 1,
});
}
}

if (options?.throw) {
throw new BetterFetchError(
response.status,
response.statusText,
isJSONResponse ? errorObject : responseText,
);
}
return {
data: null,
error: {
...errorObject,
status: response.status,
statusText: response.statusText,
return handleError({
errorContext: {
response,
responseText,
request: context,
error: {
...errorObject,
status: response.status,
statusText: response.statusText,
},
},
} as any;
hooks,
options,
url,
fetchFn: betterFetch,
cloneResponse: options?.hookOptions?.cloneResponse,
throwError: isJSONResponse ? errorObject : responseText,
}) as any;
};
6 changes: 3 additions & 3 deletions packages/better-fetch/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StandardSchemaV1 } from "./standard-schema";
import type { Schema } from "./create-fetch";
import type { BetterFetchError } from "./error";
import type { StandardSchemaV1 } from "./standard-schema";
import type { BetterFetchOption } from "./types";

export type RequestContext<T extends Record<string, any> = any> = {
Expand All @@ -20,9 +20,9 @@ export type SuccessContext<Res = any> = {
request: RequestContext;
};
export type ErrorContext = {
response: Response;
response?: Response;
request: RequestContext;
error: BetterFetchError & Record<string, any>;
error: { status: number; statusText: string } & Record<string, any>;
};
export interface FetchHooks<Res = any> {
/**
Expand Down
6 changes: 2 additions & 4 deletions packages/better-fetch/src/test/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,8 @@ describe("create-fetch-type-test", () => {
const res = await $fetch("/", {
throw: true,
});
expectTypeOf(res).toMatchTypeOf<
{ message: string }
>();
})
expectTypeOf(res).toMatchTypeOf<{ message: string }>();
});

it("should return unknown if no output is defined", () => {
const res = $fetch("/");
Expand Down
Loading