Skip to content

Commit

Permalink
Added consistent timeout and cancel behaviour to FetchRequest (#4122).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Apr 25, 2024
1 parent b553b1b commit a12a739
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 12 deletions.
36 changes: 27 additions & 9 deletions src.ts/utils/geturl-browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert } from "./errors.js";
import { assert, makeError } from "./errors.js";

import type {
FetchGetUrlFunc, FetchRequest, FetchCancelSignal, GetUrlResponse
Expand Down Expand Up @@ -27,11 +27,11 @@ declare global {
function fetch(url: string, init: FetchInit): Promise<Response>;
}

// @TODO: timeout is completely ignored; start a Promise.any with a reject?

export function createGetUrl(options?: Record<string, any>): FetchGetUrlFunc {

async function getUrl(req: FetchRequest, _signal?: FetchCancelSignal): Promise<GetUrlResponse> {
assert(signal == null || !signal.cancelled, "request cancelled before sending", "CANCELLED");

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

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

let signal: undefined | AbortSignal = undefined;
let error: null | Error = null;

const controller = new AbortController();

const timer = setTimeout(() => {
error = makeError("request timeout", "TIMEOUT");
controller.abort();
}, req.timeout);

if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => { controller.abort(); });
_signal.addListener(() => {
error = makeError("request cancelled", "CANCELLED");
controller.abort();
});
}

const init = {
method: req.method,
headers: new Headers(Array.from(req)),
body: req.body || undefined,
signal
signal: controller.signal
};

const resp = await fetch(req.url, init);
let resp: Awaited<ReturnType<typeof fetch>>;
try {
resp = await fetch(req.url, init);
} catch (_error) {
clearTimeout(timer);
if (error) { throw error; }
throw _error;
}

clearTimeout(timer);

const headers: Record<string, string> = { };
resp.headers.forEach((value, key) => {
Expand Down
24 changes: 21 additions & 3 deletions src.ts/utils/geturl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import http from "http";
import https from "https";
import { gunzipSync } from "zlib";

import { assert } from "./errors.js";
import { assert, makeError } from "./errors.js";
import { getBytes } from "./data.js";

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

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

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

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

// Create a Node-specific AbortController, if available
let abort: null | AbortController = null;
try {
abort = new AbortController();
reqOptions.abort = abort.signal;
} catch (e) { console.log(e); }

const request = ((protocol === "http") ? http: https).request(req.url, reqOptions);

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

return new Promise((resolve, reject) => {
// @TODO: Node 15 added AbortSignal; once we drop support for
// Node14, we can add that in here too

if (signal) {
signal.addListener(() => {
if (abort) { abort.abort(); }
reject(makeError("request cancelled", "CANCELLED"));
});
}

request.on("timeout", () => {
reject(makeError("request timeout", "TIMEOUT"));
});

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

0 comments on commit a12a739

Please sign in to comment.