Skip to content

Commit 962acdf

Browse files
authored
feat: ability to abort request's response (#229)
1 parent 912fe0e commit 962acdf

File tree

2 files changed

+54
-26
lines changed

2 files changed

+54
-26
lines changed

src/request.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ Deno.test("$.request", (t) => {
279279
);
280280
});
281281

282-
step("ensure times out waiting for body", async () => {
282+
step("ensure times out waiting for body", async () => {
283283
const request = new RequestBuilder()
284284
.url(new URL("/sleep-body/10000", serverUrl))
285285
.timeout(50)
@@ -294,6 +294,21 @@ Deno.test("$.request", (t) => {
294294
assertEquals(caughtErr, "Request timed out after 50 milliseconds.");
295295
});
296296

297+
step("ability to abort while waiting", async () => {
298+
const request = new RequestBuilder()
299+
.url(new URL("/sleep-body/10000", serverUrl))
300+
.showProgress();
301+
const response = await request.fetch();
302+
response.abort("Cancel.");
303+
let caughtErr: unknown;
304+
try {
305+
await response.text();
306+
} catch (err) {
307+
caughtErr = err;
308+
}
309+
assertEquals(caughtErr, "Cancel.");
310+
});
311+
297312
await Promise.all(steps);
298313
});
299314
});

src/request.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -337,30 +337,31 @@ export class RequestBuilder implements PromiseLike<RequestResponse> {
337337
}
338338
}
339339

340-
interface Timeout {
341-
signal: AbortSignal;
342-
clear(): void;
340+
interface RequestAbortController {
341+
controller: AbortController;
342+
/** Clears the timeout that may be set if there's a delay */
343+
clearTimeout(): void;
343344
}
344345

345346
/** Response of making a request where the body can be read. */
346347
export class RequestResponse {
347348
#response: Response;
348349
#downloadResponse: Response;
349350
#originalUrl: string;
350-
#timeout: Timeout | undefined;
351+
#abortController: RequestAbortController;
351352

352353
/** @internal */
353354
constructor(opts: {
354355
response: Response;
355356
originalUrl: string;
356357
progressBar: ProgressBar | undefined;
357-
timeout: Timeout | undefined;
358+
abortController: RequestAbortController;
358359
}) {
359360
this.#originalUrl = opts.originalUrl;
360361
this.#response = opts.response;
361-
this.#timeout = opts.timeout;
362+
this.#abortController = opts.abortController;
362363
if (opts.response.body == null) {
363-
this.#timeout?.clear();
364+
opts.abortController.clearTimeout();
364365
}
365366

366367
if (opts.progressBar != null) {
@@ -381,8 +382,9 @@ export class RequestResponse {
381382
pb.increment(value.byteLength);
382383
controller.enqueue(value);
383384
}
384-
if (opts.timeout?.signal?.aborted) {
385-
controller.error(opts.timeout.signal.reason);
385+
const signal = opts.abortController.controller.signal;
386+
if (signal.aborted) {
387+
controller.error(signal.reason);
386388
} else {
387389
controller.close();
388390
}
@@ -418,9 +420,10 @@ export class RequestResponse {
418420
return this.#response.redirected;
419421
}
420422

421-
/** The underlying `AbortSignal` if a delay or signal was specified. */
422-
get signal(): AbortSignal | undefined {
423-
return this.#timeout?.signal;
423+
/** The underlying `AbortSignal` used to abort the request body
424+
* when a timeout is reached or when the `.abort()` method is called. */
425+
get signal(): AbortSignal {
426+
return this.#abortController.controller.signal;
424427
}
425428

426429
/** Status code of the response. */
@@ -438,6 +441,11 @@ export class RequestResponse {
438441
return this.#response.url;
439442
}
440443

444+
/** Aborts */
445+
abort(reason?: unknown) {
446+
this.#abortController?.controller.abort(reason);
447+
}
448+
441449
/**
442450
* Throws if the response doesn't have a 2xx code.
443451
*
@@ -466,7 +474,7 @@ export class RequestResponse {
466474
}
467475
return this.#downloadResponse.arrayBuffer();
468476
} finally {
469-
this.#timeout?.clear();
477+
this.#abortController?.clearTimeout();
470478
}
471479
}
472480

@@ -483,7 +491,7 @@ export class RequestResponse {
483491
}
484492
return await this.#downloadResponse.blob();
485493
} finally {
486-
this.#timeout?.clear();
494+
this.#abortController?.clearTimeout();
487495
}
488496
}
489497

@@ -500,7 +508,7 @@ export class RequestResponse {
500508
}
501509
return await this.#downloadResponse.formData();
502510
} finally {
503-
this.#timeout?.clear();
511+
this.#abortController?.clearTimeout();
504512
}
505513
}
506514

@@ -517,7 +525,7 @@ export class RequestResponse {
517525
}
518526
return await this.#downloadResponse.json();
519527
} finally {
520-
this.#timeout?.clear();
528+
this.#abortController?.clearTimeout();
521529
}
522530
}
523531

@@ -537,7 +545,7 @@ export class RequestResponse {
537545
}
538546
return await this.#downloadResponse.text();
539547
} finally {
540-
this.#timeout?.clear();
548+
this.#abortController?.clearTimeout();
541549
}
542550
}
543551

@@ -546,7 +554,7 @@ export class RequestResponse {
546554
try {
547555
await this.readable.pipeTo(dest, options);
548556
} finally {
549-
this.#timeout?.clear();
557+
this.#abortController?.clearTimeout();
550558
}
551559
}
552560

@@ -602,7 +610,7 @@ export class RequestResponse {
602610
} catch {
603611
// do nothing
604612
}
605-
this.#timeout?.clear();
613+
this.#abortController?.clearTimeout();
606614
}
607615
} catch (err) {
608616
await this.#response.body?.cancel();
@@ -633,7 +641,12 @@ export async function makeRequest(state: RequestBuilderState) {
633641
if (state.url == null) {
634642
throw new Error("You must specify a URL before fetching.");
635643
}
636-
const timeout = getTimeout();
644+
const abortController = getTimeoutAbortController() ?? {
645+
controller: new AbortController(),
646+
clearTimeout() {
647+
// do nothing
648+
},
649+
};
637650
const response = await fetch(state.url, {
638651
body: state.body,
639652
cache: state.cache,
@@ -645,13 +658,13 @@ export async function makeRequest(state: RequestBuilderState) {
645658
redirect: state.redirect,
646659
referrer: state.referrer,
647660
referrerPolicy: state.referrerPolicy,
648-
signal: timeout?.signal,
661+
signal: abortController.controller.signal,
649662
});
650663
const result = new RequestResponse({
651664
response,
652665
originalUrl: state.url.toString(),
653666
progressBar: getProgressBar(),
654-
timeout,
667+
abortController,
655668
});
656669
if (!state.noThrow) {
657670
result.throwIfNotOk();
@@ -681,7 +694,7 @@ export async function makeRequest(state: RequestBuilderState) {
681694
}
682695
}
683696

684-
function getTimeout(): Timeout | undefined {
697+
function getTimeoutAbortController(): RequestAbortController | undefined {
685698
if (state.timeout == null) {
686699
return undefined;
687700
}
@@ -692,8 +705,8 @@ export async function makeRequest(state: RequestBuilderState) {
692705
timeout,
693706
);
694707
return {
695-
signal: controller.signal,
696-
clear() {
708+
controller,
709+
clearTimeout() {
697710
clearTimeout(timeoutId);
698711
},
699712
};

0 commit comments

Comments
 (0)