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

Add ability to cancel in-progress requests #287

Merged
merged 12 commits into from
May 15, 2017

Conversation

alextes
Copy link
Contributor

@alextes alextes commented Mar 25, 2017

  • Test canceling in progress request.
    • Call .cancel() at the right moment.
  • Check if it's possible to abort during an in-progress request.
  • Fix the PCancelable rejection blowing up instead of hitting our catch handler.
  • Document cancel option

The documentation on aborting is unfortunately pretty skimpy. It appears Node destroys the socket on abort, so it sounds like we can abort during any phase of the request.


Fixes #274

@alextes alextes changed the title Enable canceling of in progress requests (WIP) Enable canceling of in progress requests (needs feedback) Apr 1, 2017
@sindresorhus
Copy link
Owner

Yes, I think we can abort the request whenever, so I think all we have to do is make promise.cancel() call request.abort().

@sindresorhus
Copy link
Owner

Fix the PCancelable rejection blowing up instead of hitting our catch handler.

Where is this happening?

@alextes
Copy link
Contributor Author

alextes commented Apr 5, 2017

Hi, thanks for the feedback, happy to get it 😁.

$ ava test/http.js
body is a stream, piping
calling cancel

  1 failed

  cancel in-progress request promise
  node_modules/p-cancelable/index.js:65

   64:     this._canceled = true;
   65:     this._reject(new CancelError());
   66:   }

  Error: undefined

  Promise.cancel (node_modules/p-cancelable/index.js:65:16)
  Timeout._onTimeout (test/http.js:131:18)

  The .only() modifier is used in some tests. 11 tests were not run

@sindresorhus
Copy link
Owner

Would you be able to submit a failing test for p-cancelable? I never got time to dig into this. A failing test would make it much easier to investigate.

@alextes alextes force-pushed the abort-request branch 2 times, most recently from 67c7378 to a204087 Compare May 5, 2017 15:39
@alextes alextes changed the title Enable canceling of in progress requests (needs feedback) Enable canceling of in progress requests May 5, 2017
@alextes
Copy link
Contributor Author

alextes commented May 5, 2017

p-cancelable has been updated with a fix.

Final notes:

  • not sure calling abort before the request event is emitted works nicely. I used an approach that waits for the request to start then tells Node to abort.
  • p-cancelable returns immediately when asked to cancel a settled promise. I worry about the use-case where a user tries to cancel a settled promise but won't know the request went through. I can imagine that's problematic especially when a user doesn't care much about a response but is going for more of an RPC type deal.

@alextes
Copy link
Contributor Author

alextes commented May 5, 2017

Thanks, forgot to save the dep update!

@sindresorhus
Copy link
Owner

not sure about canceling before the request event is emitted. used an approach that waits for the request to start then tells Node to abort.

Is that really necessary? I think you can abort a request whenever.

p-cancelable immediately when asked to cancel a settled promise.

It immediately does what?

@alextes
Copy link
Contributor Author

alextes commented May 5, 2017

Is that really necessary? I think you can abort a request whenever.

Let me make sure and test.

It immediately does what?

Sorry. 'p-cancelable returns immediately when asked to cancel a settled promise.'

@alextes
Copy link
Contributor Author

alextes commented May 5, 2017

Heyo, I predicted an issue (starting to get the hang of this 😛) but forgot to make an effort to expand the test to catch the issue. I shelved it because the lovely version of the test doesn't work due to.. long story. Anyway, last two commits show the issue - not a pretty test - and apply the stash I had that solves the issue.

@sindresorhus
Copy link
Owner

Anyway, last two commits show the issue - not a pretty test - and apply the stash I had that solves the issue.

Ah ok. I get it now.


onCancel(() => {
req.abort();
});
Copy link
Owner

Choose a reason for hiding this comment

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

Test pass without this. Can you add a test to cover it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm. Alright. That means coming up with a way to easily test the request actually gets canceled. Don't want to simply repeat what I did in the previous test. I'll come up with something clean. Give me a couple minutes.

test/http.js Outdated
t.false(cancelRequestCompleted);
});

test.failing('cancel throws after request completed', async t => {
Copy link
Owner

Choose a reason for hiding this comment

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

What is this about?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

p-cancelable returns immediately when asked to cancel a settled promise. I worry about the use-case where a user tries to cancel a settled promise but won't know the request went through. I can imagine that's problematic especially when a user doesn't care much about a response but is going for more of an RPC type deal.

Copy link
Owner

Choose a reason for hiding this comment

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

It works exactly like trying to reject a settled promise. When I promise is settled, it's by the name of it settled. Same with cancel. If the user use .cancel(), they should be aware they might call it too late and handle that themselves if they need. We can document this well. They can even check the promise whether it was canceled or not with promise.canceled (Which is false if you try canceling it after the promise is settled). I think p.cancel() throwing based on timing could lead to race issues and would be surprising to the user.

Please call me out if you think I'm totally wrong in this. I'm happy to discuss.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did a little testing and a little thinking. Trying to resolve an already settled promise seems to do nothing in the case of Node too. A note in the documentation is enough.
I think you're right on this.

@alextes alextes force-pushed the abort-request branch 3 times, most recently from cc70b02 to 577caff Compare May 6, 2017 00:31
@sindresorhus
Copy link
Owner

@alextes In case you missed it, note: #287 (comment)

@alextes
Copy link
Contributor Author

alextes commented May 6, 2017

Not sure where to leave the documentation for this. It's a sort of indirect API. I took my best guess.

@alextes alextes force-pushed the abort-request branch 2 times, most recently from 8b5d113 to ba00120 Compare May 6, 2017 15:40
test/http.js Outdated
p.cancel();

await t.throws(p, PCancelable.CancelError);
await t.notThrows(() => cancelPromise);
Copy link
Owner

Choose a reason for hiding this comment

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

await t.notThrows(cancelPromise);

test/http.js Outdated
const body = new Readable({
read() {}
});
body.push(String(1));
Copy link
Owner

Choose a reason for hiding this comment

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

body.push('1');

test/http.js Outdated
cancelPromise = new Promise((resolve, reject) => {
req.on('aborted', resolve);
res.on('finish', () => {
reject(new Error('request finished instead of aborting'));
Copy link
Owner

Choose a reason for hiding this comment

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

request => Request

test/http.js Outdated
@@ -1,8 +1,13 @@
import stream from 'stream';
import PCancelable from 'p-cancelable';
Copy link
Owner

Choose a reason for hiding this comment

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

Place this after from 'ava'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, so we alphabetize after complying with the import plugin rules?

Copy link
Owner

Choose a reason for hiding this comment

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

No, I just prefer the order:

  • builtins
  • ava (placed here because it's the main external import)
  • external
  • local

readme.md Outdated
@@ -356,6 +356,9 @@ request(`https://${config.host}/production/`, {
});
```

## Aborting the request
Copy link
Owner

Choose a reason for hiding this comment

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

Can you place this above the Proxies section?

Copy link
Owner

Choose a reason for hiding this comment

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

Maybe we should say Canceling the request instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done :3.
I don't think we should. It sounds worse, granted, but Node consistently calls it 'abort'.

readme.md Outdated
@@ -356,6 +356,9 @@ request(`https://${config.host}/production/`, {
});
```

## Aborting the request

The promise returned by got features a `.cancel` function. It takes no arguments and will abort the request, destroying the underlying socket if called.
Copy link
Owner

Choose a reason for hiding this comment

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

got => Got

.cancel => .cancel()


I would make it more succinct:

The promise returned by Got has a .cancel() function, which when called, aborts the request.

index.js Outdated
@@ -357,6 +371,7 @@ function stdError(error, opts) {

got.RequestError = createErrorClass('RequestError', stdError);
got.ReadError = createErrorClass('ReadError', stdError);
got.InvalidCancelError = createErrorClass('InvalidCancelError', stdError);
Copy link
Owner

Choose a reason for hiding this comment

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

We don't need this anymore.

test/http.js Outdated
});

await t.throws(p, PCancelable.CancelError);
await t.notThrows(() => cancelPromise);
Copy link
Owner

Choose a reason for hiding this comment

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

await t.notThrows(cancelPromise);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just tried this. The test fails without. Turned it back into what it was. I remember now I wrote it your suggested way first. Not sure why this demands to be lazily evaluated. My JS-foo is not strong enough (yet).

Copy link
Owner

Choose a reason for hiding this comment

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

@alextes t.notThrows either accepts a promise that rejects or a function that throws. AVA is actually catching a bug in your test here as cancelPromise is null, not a Promise. Try: console.log(cancelPromise === null);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course. I'll fix it.

Copy link
Owner

Choose a reason for hiding this comment

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

Ping :)

@sindresorhus
Copy link
Owner

Can you mention cancelation in the readme intro?

package.json Outdated
@@ -63,6 +64,7 @@
"devDependencies": {
"ava": "^0.18.0",
"coveralls": "^2.11.4",
"delay": "^2.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤦‍♀️

@alextes alextes force-pushed the abort-request branch 2 times, most recently from 90232f5 to e4b008b Compare May 6, 2017 20:38
@sindresorhus
Copy link
Owner

Can you add commits instead of rebasing? Makes it easier to see what changed. We'll squash on merge anyways.

alextes added 3 commits May 6, 2017 22:40
Uses a stream for the body that never closes. Adds a number of
console.log expressions for debugging of test control flows.
Adds the dependency and switches out the main got promise for a
PCancelable. Adds canceling as soon as the request starts. Disables
canceling as soon as the response ends.
@alextes
Copy link
Contributor Author

alextes commented May 6, 2017

Sure, will do that in the future. I asked you on Gitter when you squash and when you don't. The only reason I was rebasing is because I wasn't sure if you'd squash.

@alextes
Copy link
Contributor Author

alextes commented May 6, 2017

I'll see about reviewing my own PR in the morning next time I'm coding late. Too many slip ups from code that changed heavily during the hour I was trying to find a strong way to write these tests.

@alextes
Copy link
Contributor Author

alextes commented May 6, 2017

Alright, ready for re-review @sindresorhus.

@sindresorhus sindresorhus requested a review from floatdrop May 7, 2017 11:48
@alextes alextes changed the title Enable canceling of in progress requests Enable canceling of in progress requests (needs feedback) May 13, 2017
@alextes
Copy link
Contributor Author

alextes commented May 13, 2017

So I finally finished trying out all ideas I could come up with for solid tests. The in-flight one looks quite solid to me now. It starts a streamed body request, doesn't close the stream until the got triggers the cancel. To make sure we abort on cancel the test then verifies the server emitted an abort event, not a finish event.

The immediate cancel test is much harder. First off, by 'immediate cancel' I mean a user canceling a request before got's underlying event emitter has even emitted its request event. I tried all ideas I had, but the core issue remains. When you cancel a request, you end up with a rejected promise and pretty much lose the opportunity to get any further feedback. After extensive testing, it seems the 'connection' event is the event that hits a Node HTTP server first. This event doesn't fire with an immediate cancel. In other words, unless more of got's internals are exposed it appears impossible to make sure got ever triggered the abort. The compromise I've currently gone with is to make the following assumption: if the server has not received a request within 1000ms, the abort was executed client-side.

Any better method of testing is very welcome, as are of course any pointers to clean up the code more. Sorry for taking so long, this PR has proven to be a very tough one to crack.

@sindresorhus
Copy link
Owner

Any better method of testing is very welcome, as are of course any pointers to clean up the code more.

I can't think of any better way either.

@sindresorhus sindresorhus changed the title Enable canceling of in progress requests (needs feedback) Add ability to cancel in-progress requests May 15, 2017
@sindresorhus sindresorhus merged commit 9ef7a5a into sindresorhus:master May 15, 2017
@sindresorhus
Copy link
Owner

This is great stuff @alextes. Really appreciate all the hard work and thoughts you put into it. 🙌

superhighfive

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.

2 participants