-
-
Notifications
You must be signed in to change notification settings - Fork 1k
[WIP] Add abort support #437
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import Headers, { exportNodeCompatibleHeaders } from './headers.js'; | |
import Body, { clone, extractContentType, getTotalBytes } from './body'; | ||
|
||
const { format: format_url, parse: parse_url } = require('url'); | ||
const { AbortController, AbortSignal } = require('abort-controller'); | ||
|
||
const INTERNALS = Symbol('Request internals'); | ||
|
||
|
@@ -27,6 +28,14 @@ function isRequest(input) { | |
); | ||
} | ||
|
||
// TODO: use toString check after https://github.com/mysticatea/abort-controller/pull/5 | ||
function isAbortSignal(input) { | ||
return ( | ||
typeof input === 'object' && | ||
input instanceof AbortSignal | ||
); | ||
} | ||
|
||
/** | ||
* Request class | ||
* | ||
|
@@ -37,6 +46,7 @@ function isRequest(input) { | |
export default class Request { | ||
constructor(input, init = {}) { | ||
let parsedURL; | ||
let signal; | ||
|
||
// normalize input | ||
if (!isRequest(input)) { | ||
|
@@ -52,6 +62,14 @@ export default class Request { | |
input = {}; | ||
} else { | ||
parsedURL = parse_url(input.url); | ||
signal = input[INTERNALS].signal; | ||
} | ||
|
||
if (init.signal != null) { | ||
if (!isAbortSignal(init.signal)) { | ||
throw new TypeError('Provided signal must be an AbortSignal object'); | ||
} | ||
signal = init.signal; | ||
} | ||
|
||
let method = init.method || input.method || 'GET'; | ||
|
@@ -82,11 +100,23 @@ export default class Request { | |
} | ||
} | ||
|
||
const abortController = new AbortController(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could someone explain why we are initializing another internal AbortController? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per spec, the |
||
if (signal !== undefined) { | ||
if (signal.aborted) { | ||
abortController.abort(); | ||
} else { | ||
signal.addEventListener('abort', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This eventListener is never removed, is it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it could be of any help maybe adding {once: true} to addEventListener could help cleaning up things There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mika-fischer No, and it's very difficult to do so. We could add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But won't this cause issues, for instance in the case where I have a long running Node.js application where I have a global AbortController (to be able to abort all requests when the application shuts down) and each request in the whole lifetime of the app (let's say hundreds of thousands) adds a listener, none of which is ever removed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😔 yes. It’s also one of the reasons why this is a WIP. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you just save a cleanup closure (which just removes the listener from the parent AbortController) and attach this closure to the appropriate events of the Node.js Request/Response so that it gets called when the request is finished? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A cleanup closure would remove the listener from the parent AbortController after fetch() terminates for any reason, right? Would that affect the ability to cancel a fetch that reuses a Request object for multiple fetches? Does that maybe mean that the listener should be added each time the fetch begins and removed when the fetch terminates? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
abortController.abort(); | ||
}); | ||
} | ||
} | ||
|
||
this[INTERNALS] = { | ||
method, | ||
redirect: init.redirect || input.redirect || 'follow', | ||
headers, | ||
parsedURL | ||
parsedURL, | ||
signal: abortController.signal | ||
}; | ||
|
||
// node-fetch-only options | ||
|
@@ -116,6 +146,10 @@ export default class Request { | |
return this[INTERNALS].redirect; | ||
} | ||
|
||
get signal() { | ||
return this[INTERNALS].signal; | ||
} | ||
|
||
/** | ||
* Clone this request | ||
* | ||
|
@@ -143,6 +177,16 @@ Object.defineProperties(Request.prototype, { | |
clone: { enumerable: true } | ||
}); | ||
|
||
/** | ||
* Get the AbortSignal object belonging to a Request. | ||
* | ||
* @param Request A Request instance | ||
* @return AbortSignal request's signal | ||
*/ | ||
export function getAbortSignal(request) { | ||
return request[INTERNALS].signal; | ||
} | ||
|
||
/** | ||
* Convert a Request to Node.js http request options. | ||
* | ||
|
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.
any way this could be more inline with the browser spec? rejecting with an
AbortError
? when using with isomorphic fetch this would result in having to catch two types of errors for node and browserhttps://dom.spec.whatwg.org/#aborting-ongoing-activities