Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actually cancel the request after the timeout #27

Merged
merged 14 commits into from
Feb 4, 2019

Conversation

ashinzekene
Copy link
Contributor

Fixes #8

@sindresorhus
Copy link
Owner

Awesome! Can you add a test?

@sindresorhus sindresorhus changed the title cancel request after timeout Actually cancel the request after the timeout Sep 10, 2018
index.js Outdated
@@ -99,6 +105,10 @@ class Ky {
...otherOptions
};

if (supportsCancel) {
this._options.signal = abortCtrl.abort();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would override the user's signal.

index.js Outdated
@@ -1,6 +1,7 @@
'use strict';

const isObject = value => value !== null && typeof value === 'object';
const supportsCancel = typeof AbortController === 'function';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supportsCancel => supportsAbortController

index.js Outdated
@@ -77,6 +78,11 @@ class TimeoutError extends Error {
}
}

let abortCtrl;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abortCtrl => abortController

@ashinzekene
Copy link
Contributor Author

I need help with writing the tests

index.js Outdated
@@ -99,6 +105,10 @@ class Ky {
...otherOptions
};

if (supportsAbortController && !this._options.signal) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still want timeout to work, even if the user sets their own signal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ookay....

index.js Outdated Show resolved Hide resolved
Copy link
Contributor Author

@ashinzekene ashinzekene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting is failing because of this line

There is no addEventListener method on type AbortSignal

@taylorjdawson
Copy link

How is this PR coming along, @ashinzekene anything I can do to help?

@ashinzekene
Copy link
Contributor Author

I'm waiting for a review. You can also look at helping with tests

@sindresorhus
Copy link
Owner

@felixfbecker You have experience with AbortController, is this the correct way to handle it?

Copy link

@felixfbecker felixfbecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signal should probably also be passed to delay() in the retry logic

index.js Outdated
@@ -77,12 +79,21 @@ class TimeoutError extends Error {
}
}

let abortController;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A global AbortController for all requests? That seems wrong. It should be one controller per request

index.js Outdated
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const timeout = (promise, ms) => Promise.race([
promise,
(async () => {
await delay(ms);
if (!isAborted && supportsAbortController) {
abortController.abort();
isAborted = true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't isAborted the same as abortController.signal.aborted?

index.js Outdated
@@ -99,6 +110,14 @@ class Ky {
...otherOptions
};

if (supportsAbortController) {
if (this._options.signal) {
this._options.signal.addEventListener('abort', () => !isAborted && abortController.abort());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abort() is idempotent so the isAborted check is not needed.

@ashinzekene
Copy link
Contributor Author

I believe this fixes the issues right ? @felixfbecker

Copy link

@felixfbecker felixfbecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a test for this? For example, if you set the timeout 0 or 1ms then wait, you should be able to assert that the Promise rejects with a TimeoutError.
Related, since this touches the AbortSignal, there should probably be a test that asserts you can still pass an AbortSignal manually (i.e. Promise is rejected with AbortError if signal is aborted).

index.js Show resolved Hide resolved
@ashinzekene
Copy link
Contributor Author

ashinzekene commented Oct 3, 2018 via email

@ashinzekene
Copy link
Contributor Author

I'm finding it hard to test. I initially had to installed browser-env to be able to use AbortController on the test file, but then this still return undefined

@felixfbecker
Copy link

You shouldn't need a whole browser env with DOM etc for this. https://www.npmjs.com/package/abort-controller should be enough.

@ianwalter
Copy link
Contributor

@felixfbecker I don't think node-fetch supports AbortController yet node-fetch/node-fetch#437 and I doubt jsdom does either.

It sounds like support for testing in a real browser will land in ky soon: #34

@ashinzekene
Copy link
Contributor Author

Cool @ianwalter

I found out that the tests I wrote kept failing. Didn't know if the issue was from node-fetch or abort-controller polyfill.

Was about raising the issue here

index.js Outdated
promise,
(async () => {
await delay(ms);
if (supportsAbortController) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (abortController) {

index.js Outdated Show resolved Hide resolved
@szmarczak
Copy link
Collaborator

LGTM, we need only to test it on browsers somehow...

@sindresorhus
Copy link
Owner

@ianwalter I've added browser testing, so you can use this now to implement the tests: f0b7502 :)

index.js Outdated
if (supportsAbortController) {
abortController = new AbortController();
if (this._options.signal) {
this._options.signal.addEventListener('abort', () => abortController.abort());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This listener would need to be removed once the request is completed (or errored, i.e. in a finally), otherwise the listener would leak and stay around forever even though the request cannot be cancelled anymore

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe it or not, this was never fixed before the PR landed so we ran into this memory leak in our application in 2024! 😄

index.js Outdated
let abortController;

if (supportsAbortController) {
abortController = new AbortController();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abortController should be defined as a const, since it's never re-assigned.

@sindresorhus
Copy link
Owner

@ianwalter Still interested in finishing this? :)

@ashinzekene
Copy link
Contributor Author

I've pushed fixes for this. But I need help writing tests. Can someone assist?
@felixfbecker @szmarczak @ianwalter

@ianwalter
Copy link
Contributor

ianwalter commented Dec 15, 2018

I tried writing a test for this but doesn't look like headless cypress run (electron 59) supports AbortController. Will play around with it some more.

Update: Not 100% sure but seems like we should just wait for Cypress v4 which will upgrade Electron to v3 which will use Chrome v66 which will add support for this. I read that you can't polyfill AbortController with native fetch so that's doesn't seem like an option. Other option is to use puppeteer/karmatic instead of Cypress.

@felixfbecker
Copy link

Yeah if you just assign AbortController on window fetch will not honor the signal. Do the test make actual HTTP requests though? What if fetch is also polyfilled (the polyfill can use xhr under the hood which is cancellable)?

@ashinzekene
Copy link
Contributor Author

ashinzekene commented Dec 15, 2018

When I tried writing tests for it, I found out that it actually works.
I wrote this

	it('throws abort error when aborted by user', async () => {
		const abortController = new AbortController();
		const {signal} = abortController;
		const response = ky('https://jsonplaceholder.typicode.com/todos', {signal});
		expect(() => abortController.abort()).to.throw(window.AbortError);
		await response;
	});

this from cypress
image

The test fails but the request is actually aborted and I can't find a way to catch the error

However, if i write the test this way

	it('throws abort error when aborted by user', async () => {
		const abortController = new AbortController();
		const {signal} = abortController;
		const response = ky('https://jsonplaceholder.typicode.com/todos', {signal});
		expect(abortController.abort).to.throw(window.AbortError);
		await response;
	});

It passes but the request is not aborted
image

@ianwalter
Copy link
Contributor

I would have had a couple of working tests thanks to @felixfbecker's suggestion but forgot that using fetch (even if it's a polyfill) doesn't work with cy.route stubbing (cypress-io/cypress#95) so I have to find another way to handle the request and assert it's aborted.

@sholladay
Copy link
Collaborator

@ianwalter have you seen this example of stubbing fetch in cy.visit() instead of cy.route()? https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/stubbing-spying__window-fetch/cypress/integration/stub-fetch-spec.js#L17-L23

@felixfbecker
Copy link

Since when does window.AbortError exist?

@ashinzekene
Copy link
Contributor Author

Sorry, there's actually nothing like that. I got the error from Cypress. I think its a type of DOMException

@felixfbecker
Copy link

error.name will be AbortError

@sindresorhus
Copy link
Owner

Bump :)

@sindresorhus sindresorhus merged commit 8f17a13 into sindresorhus:master Feb 4, 2019
@sindresorhus
Copy link
Owner

Thank you, @ashinzekene :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants