-
-
Notifications
You must be signed in to change notification settings - Fork 957
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
Conversation
Yes, I think we can abort the request whenever, so I think all we have to do is make |
Where is this happening? |
Hi, thanks for the feedback, happy to get it 😁.
|
Would you be able to submit a failing test for |
67c7378
to
a204087
Compare
Final notes:
|
Thanks, forgot to save the dep update! |
Is that really necessary? I think you can abort a request whenever.
It immediately does what? |
Let me make sure and test.
Sorry. ' |
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. |
Ah ok. I get it now. |
|
||
onCancel(() => { | ||
req.abort(); | ||
}); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this about?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cc70b02
to
577caff
Compare
@alextes In case you missed it, note: #287 (comment) |
Not sure where to leave the documentation for this. It's a sort of indirect API. I took my best guess. |
8b5d113
to
ba00120
Compare
test/http.js
Outdated
p.cancel(); | ||
|
||
await t.throws(p, PCancelable.CancelError); | ||
await t.notThrows(() => cancelPromise); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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')); |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await t.notThrows(cancelPromise);
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping :)
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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦♀️
90232f5
to
e4b008b
Compare
Can you add commits instead of rebasing? Makes it easier to see what changed. We'll squash on merge anyways. |
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.
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. |
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. |
Alright, ready for re-review @sindresorhus. |
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. |
I can't think of any better way either. |
This is great stuff @alextes. Really appreciate all the hard work and thoughts you put into it. 🙌 |
.cancel()
at the right moment.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