Skip to content

Commit

Permalink
chore: handle native fetch timeout error
Browse files Browse the repository at this point in the history
  • Loading branch information
nflaig committed Aug 12, 2023
1 parent 3c7b815 commit df24f75
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 6 deletions.
19 changes: 18 additions & 1 deletion packages/api/src/utils/client/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function isFetchError(e: unknown): e is FetchError {
return e instanceof FetchError;
}

export type FetchErrorType = "failed" | "input" | "aborted" | "unknown";
export type FetchErrorType = "failed" | "input" | "timeout" | "aborted" | "unknown";

type FetchErrorCause = NativeFetchFailedError["cause"] | NativeFetchInputError["cause"];

Expand All @@ -38,6 +38,10 @@ export class FetchError extends Error {
this.type = "input";
this.code = e.cause.code || "ERR_INVALID_INPUT";
this.cause = e.cause;
} else if (isNativeFetchTimeoutError(e)) {
super(`Request to ${url.toString()} timed out`);
this.type = "timeout";
this.code = "ERR_TIMEOUT";
} else if (isNativeFetchAbortError(e)) {
super(`Request to ${url.toString()} was aborted`);
this.type = "aborted";
Expand Down Expand Up @@ -120,6 +124,15 @@ type NativeFetchAbortError = DOMException & {
name: "AbortError";
};

/**
* ```
* DOMException [TimeoutError]: The operation was aborted due to timeout
* ```
*/
type NativeFetchTimeoutError = DOMException & {
name: "TimeoutError";
};

function isNativeFetchError(e: unknown): e is NativeFetchError {
return e instanceof TypeError && (e as NativeFetchError).cause instanceof Error;
}
Expand All @@ -135,3 +148,7 @@ function isNativeFetchInputError(e: unknown): e is NativeFetchInputError {
function isNativeFetchAbortError(e: unknown): e is NativeFetchAbortError {
return e instanceof DOMException && e.name === "AbortError";
}

function isNativeFetchTimeoutError(e: unknown): e is NativeFetchTimeoutError {
return e instanceof DOMException && e.name === "TimeoutError";
}
26 changes: 21 additions & 5 deletions packages/api/test/unit/client/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("FetchError", function () {
url?: string;
requestListener?: http.RequestListener;
abort?: true;
timeout?: number;
timeout?: true;
errorType: FetchErrorType;
errorCode: string;
expectCause: boolean;
Expand Down Expand Up @@ -74,6 +74,16 @@ describe("FetchError", function () {
errorCode: "ERR_ABORTED",
expectCause: false,
},
{
id: "Timeout request",
timeout: true,
requestListener: () => {
// leave the request open until timeout
},
errorType: "timeout",
errorCode: "ERR_TIMEOUT",
expectCause: false,
},
];

const afterHooks: (() => Promise<void>)[] = [];
Expand All @@ -90,7 +100,7 @@ describe("FetchError", function () {
});

for (const testCase of testCases) {
const {id, url = `http://localhost:${port}`, requestListener, abort} = testCase;
const {id, url = `http://localhost:${port}`, requestListener, abort, timeout} = testCase;

it(id, async function () {
if (requestListener) {
Expand All @@ -107,9 +117,15 @@ describe("FetchError", function () {
);
}

const controller = new AbortController();
if (abort) setTimeout(() => controller.abort(), 20);
await expect(fetch(url, {signal: controller.signal})).to.be.rejected.then((error: FetchError) => {
let signal: AbortSignal | undefined;
if (abort) {
const controller = new AbortController();
setTimeout(() => controller.abort(), 0);
signal = controller.signal;
} else if (timeout) {
signal = AbortSignal.timeout(10);
}
await expect(fetch(url, {signal})).to.be.rejected.then((error: FetchError) => {
expect(error.type).to.be.equal(testCase.errorType);
expect(error.code).to.be.equal(testCase.errorCode);
if (testCase.expectCause) {
Expand Down

0 comments on commit df24f75

Please sign in to comment.