Skip to content

Commit a12a739

Browse files
committed
Added consistent timeout and cancel behaviour to FetchRequest (#4122).
1 parent b553b1b commit a12a739

File tree

2 files changed

+48
-12
lines changed

2 files changed

+48
-12
lines changed

src.ts/utils/geturl-browser.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assert } from "./errors.js";
1+
import { assert, makeError } from "./errors.js";
22

33
import type {
44
FetchGetUrlFunc, FetchRequest, FetchCancelSignal, GetUrlResponse
@@ -27,11 +27,11 @@ declare global {
2727
function fetch(url: string, init: FetchInit): Promise<Response>;
2828
}
2929

30-
// @TODO: timeout is completely ignored; start a Promise.any with a reject?
31-
3230
export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {
3331

3432
async function getUrl(req: FetchRequest, _signal?: FetchCancelSignal): Promise<GetUrlResponse> {
33+
assert(signal == null || !signal.cancelled, "request cancelled before sending", "CANCELLED");
34+
3535
const protocol = req.url.split(":")[0].toLowerCase();
3636

3737
assert(protocol === "http" || protocol === "https", `unsupported protocol ${ protocol }`, "UNSUPPORTED_OPERATION", {
@@ -43,21 +43,39 @@ export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {
4343
operation: "request"
4444
});
4545

46-
let signal: undefined | AbortSignal = undefined;
46+
let error: null | Error = null;
47+
48+
const controller = new AbortController();
49+
50+
const timer = setTimeout(() => {
51+
error = makeError("request timeout", "TIMEOUT");
52+
controller.abort();
53+
}, req.timeout);
54+
4755
if (_signal) {
48-
const controller = new AbortController();
49-
signal = controller.signal;
50-
_signal.addListener(() => { controller.abort(); });
56+
_signal.addListener(() => {
57+
error = makeError("request cancelled", "CANCELLED");
58+
controller.abort();
59+
});
5160
}
5261

5362
const init = {
5463
method: req.method,
5564
headers: new Headers(Array.from(req)),
5665
body: req.body || undefined,
57-
signal
66+
signal: controller.signal
5867
};
5968

60-
const resp = await fetch(req.url, init);
69+
let resp: Awaited<ReturnType<typeof fetch>>;
70+
try {
71+
resp = await fetch(req.url, init);
72+
} catch (_error) {
73+
clearTimeout(timer);
74+
if (error) { throw error; }
75+
throw _error;
76+
}
77+
78+
clearTimeout(timer);
6179

6280
const headers: Record<string, string> = { };
6381
resp.headers.forEach((value, key) => {

src.ts/utils/geturl.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import http from "http";
22
import https from "https";
33
import { gunzipSync } from "zlib";
44

5-
import { assert } from "./errors.js";
5+
import { assert, makeError } from "./errors.js";
66
import { getBytes } from "./data.js";
77

88
import type {
@@ -15,6 +15,8 @@ import type {
1515
export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {
1616

1717
async function getUrl(req: FetchRequest, signal?: FetchCancelSignal): Promise<GetUrlResponse> {
18+
// Make sure we weren't cancelled before sending
19+
assert(signal == null || !signal.cancelled, "request cancelled before sending", "CANCELLED");
1820

1921
const protocol = req.url.split(":")[0].toLowerCase();
2022

@@ -35,6 +37,13 @@ export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {
3537
if (options.agent) { reqOptions.agent = options.agent; }
3638
}
3739

40+
// Create a Node-specific AbortController, if available
41+
let abort: null | AbortController = null;
42+
try {
43+
abort = new AbortController();
44+
reqOptions.abort = abort.signal;
45+
} catch (e) { console.log(e); }
46+
3847
const request = ((protocol === "http") ? http: https).request(req.url, reqOptions);
3948

4049
request.setTimeout(req.timeout);
@@ -45,8 +54,17 @@ export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {
4554
request.end();
4655

4756
return new Promise((resolve, reject) => {
48-
// @TODO: Node 15 added AbortSignal; once we drop support for
49-
// Node14, we can add that in here too
57+
58+
if (signal) {
59+
signal.addListener(() => {
60+
if (abort) { abort.abort(); }
61+
reject(makeError("request cancelled", "CANCELLED"));
62+
});
63+
}
64+
65+
request.on("timeout", () => {
66+
reject(makeError("request timeout", "TIMEOUT"));
67+
});
5068

5169
request.once("response", (resp: http.IncomingMessage) => {
5270
const statusCode = resp.statusCode || 0;

0 commit comments

Comments
 (0)