-
Notifications
You must be signed in to change notification settings - Fork 27
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
Hybrid node:http + fetch approach to node-fetch-server #33
base: main
Are you sure you want to change the base?
Conversation
…tch-hybrid-server
…n benchmark runner
… and hybrid FetchIncomingMessage constructpor for node:http
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 think this is a good start, and the results from the benchmark are encouraging! I requested a few changes.
/** | ||
* **DO NOT USE**, will always throw. Kept only for compatibility with type definitions. | ||
* | ||
* @deprecated Use `Request.formDataAsync()` instead which returns an `AsyncGenerator<MultipartPart>` |
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.
Where is Request.formDataAsync
defined? Is that a new spec? Or are you using the form-data-parser package?
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'm aiming to use the other packages in the repo as much as possible overall so yes, the goal is to use form-data-parser
.
I think I meant to use it here but left it for a future iteration. I'd like to confirm this first though, as simplifying the Request model in use would allow me to handle this differently:
// const ac = new AbortController(); | ||
// this.on('close', () => { | ||
// ac.abort(); | ||
// }); | ||
|
||
// let signal = ac.signal; | ||
// this.#signal = ac.signal; | ||
|
||
// if (signal.aborted) { | ||
// ac.abort(signal.reason); | ||
// } else { | ||
// // Keep a strong ref to ac while request object | ||
// // is alive. This is needed to prevent AbortController | ||
// // from being prematurely garbage collected. | ||
// // See, https://github.com/nodejs/undici/issues/1926. | ||
// this[kAbortController] = ac; | ||
// } |
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.
Is this supposed to be uncommented? Seems like it, otherwise request.signal
will be undefined
.
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.
There's A LOT of code in Undici that doesn't make sense in a server setting because fetch is a browser-centric spec. I may have been too liberal in cutting things out, will double check.
QQ: would using the actual Request
class from Undici be a hard requirement or explicit goal here? There's further perf improvements to be had by using plain objects or a simpler class.
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.
A non exhaustive list:
-
AbortController / Signals
AbortController
,AbortSignal
, finalization logic,dependentControllerMap
,buildAbort
, etc.- ❓Under which circumstances would server-side code need to abort an incoming request?
-
Referrer / ReferrerPolicy / Mode / Credentials / Cache / Redirect / Integrity / Keepalive
- teh implementation in the PR builds an instance of
SuperHeaders
from parser events. The additional logic in Undici is client-side specific.
- teh implementation in the PR builds an instance of
-
Environment Settings Objects / sameOrigin Checks
- Again, these are client or spec-level concerns not relevant on the server.
-
makeRequest()
,cloneRequest()
,fromInnerRequest()
- Methods used internally by Undici to construct new
Request
objects with spec-defined state. - the implementation in the PR builds incoming requests from events emitted by the
node:http
parser, not from a “clone” or “init” object.
- Methods used internally by Undici to construct new
-
webidl.converters.RequestInfo
/webidl.converters.RequestInit
- similar as previous point, request is built bit by bit from parser events
return http.createServer<IncomingMessage>( | ||
{ IncomingMessage }, |
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.
Interesting! I had no idea the createServer
API could use a custom IncomingMessage
class. Very cool find 👍
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.
Thanks! Perusing the source code taught me a bunch of interesting bits about Node internals 😁
}, | ||
"scripts": { | ||
"bench": "bash ./bench/runner.sh", | ||
"build": "tsup", | ||
"test": "node --import @swc-node/register/esm-register --test ./src/**/*.spec.ts", | ||
"debug": "node --import @swc-node/register/esm-register ./src/test.ts", |
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 looks like ./src/test.ts
is missing?
|
||
// hybrid magic | ||
export { FetchIncomingMessage } from './lib/fetch-incoming-message/index.js'; | ||
export { createFetchServer } from './lib/create-fetch-server.js'; |
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.
Note: Ideally instead of creating two APIs we'd stick with a single API, createRequestListenener
. Although I understand that you're exporting createFetchServer
here under a different name so you can benchmark them both together.
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.
Exactly, and to allow iteration without breaking the existing implementation. The ultimate goal is of course to merge it into a single export once ready
get host() { | ||
return this.get('host'); | ||
} |
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 we please put this in a separate PR, and add a setter as well? Thanks 🙏
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 think you've made this not needed in a recent release. I can remove this from the PR in favor of what's in the headers package now.
NOTE: all changes in this PR are non-breaking and implemented as separate, parallel exports except for:
added getter for the Host header to
@mjackson/headers
updated
platform
field inpackages/node-fetch-server/tsup.config.ts
to'node
implemented a custom
IncomingMessage
class that anode:http
server can use as-is, and that implements theRequest
interface fromundici-types
5-10% faster than the base implementation in
node-fetch-server
15-20% faster than
Express
added some basic unit tests to check parsing of sample requests works