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

Hybrid node:http + fetch approach to node-fetch-server #33

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

cbnsndwch
Copy link

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 in packages/node-fetch-server/tsup.config.ts to 'node

  • implemented a custom IncomingMessage class that a node:http server can use as-is, and that implements the Request interface from undici-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

Copy link
Owner

@mjackson mjackson left a 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>`
Copy link
Owner

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?

Copy link
Author

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:

#33 (comment)

Comment on lines +44 to +60
// 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;
// }
Copy link
Owner

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.

Copy link
Author

@cbnsndwch cbnsndwch Jan 11, 2025

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.

Copy link
Author

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.
  • 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.
  • webidl.converters.RequestInfo / webidl.converters.RequestInit

    • similar as previous point, request is built bit by bit from parser events

Comment on lines +39 to +40
return http.createServer<IncomingMessage>(
{ IncomingMessage },
Copy link
Owner

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 👍

Copy link
Author

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",
Copy link
Owner

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';
Copy link
Owner

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.

Copy link
Author

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

Comment on lines +257 to +259
get host() {
return this.get('host');
}
Copy link
Owner

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 🙏

Copy link
Author

@cbnsndwch cbnsndwch Jan 11, 2025

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.

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