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

refactor: use base node http types #723

Closed
wants to merge 13 commits into from

Conversation

cdaringe
Copy link

@cdaringe cdaringe commented Mar 3, 2022

Description

Re-post of #721 against v3

  • Refactor http-proxy-middleware middeware types to be properly x-server compatible, from both a typescript & node.js lens
  • Augment http.IncomingMessage with http-proxy-middleware values permissible on req, but not present by default

BREAKING CHANGE: RequestHandler => RequestMiddleware. Middleware now
is expressed in terms of node core http.* types, which express req/res
are compatible with. Orienting to base http.* types allows better typing
across all node servers, as eventually each implementation has these
values in their servers. That is to say, express, koa, fastify, next,
etc all use http.IncomingMessage/http.ServerResponse under the hood,
thus the new middleware types are compatible with everyone.

Technically this should be backwards compatible for most users. However, exotic coupling to the previous types (e.g. Parameters<typeof createProxyMiddleware> could induce subtle breakages.

Motivation and Context

See #719

How has this been tested?

  • Mo' tests added!

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

chimurai and others added 8 commits February 19, 2022 23:38
BREAKING CHANGE: `RequestHandler` => `RequestMiddleware`. Middleware now
is expressed in terms of node core http.* types, which express req/res
are compatible with. Orienting to base http.* types allows better typing
accross all node servers, as eventually each implementation has these
values in their servers. That is to say, express, koa, fastify, next,
etc all use http.IncomingMessage/http.ServerResponse under the hood,
thus the new middleware types are compatible with everyone.
BREAKING CHANGE: `RequestHandler` => `RequestMiddleware`. Middleware now
is expressed in terms of node core http.* types, which express req/res
are compatible with. Orienting to base http.* types allows better typing
accross all node servers, as eventually each implementation has these
values in their servers. That is to say, express, koa, fastify, next,
etc all use http.IncomingMessage/http.ServerResponse under the hood,
thus the new middleware types are compatible with everyone.
@cdaringe cdaringe mentioned this pull request Mar 3, 2022
5 tasks
@cdaringe
Copy link
Author

@chimurai , any issues, or g2g? saw tasks were cancelled, but tests were lookin good in CI

@chimurai
Copy link
Owner

chimurai commented Mar 12, 2022

Hi @cdaringe.

The tests were running indefinitely in github-actions. I cancelled them after a while. Think it's because the server is still running after the test finished. (just found out I had to submit the review :) )
Re-ran the test. You'll see it's running for more than 30minutes. While they should finish in less than a minute. (https://github.com/chimurai/http-proxy-middleware/actions/runs/1929895626)

Haven't got time yet to do a thorough review. I'll try see if I can do it soon

test/e2e/servers/http.spec.ts Outdated Show resolved Hide resolved
@cdaringe
Copy link
Author

hmm, odd. locally, they all exit fine. i sent a patch w/ server.close. lets see if it takes. i thought you had cancelled them for other reasons 😆

@coveralls
Copy link

Coverage Status

Coverage remained the same at 98.795% when pulling 1ddcd6b on cdaringe:refactor/types-v3 into 0b30c5d on chimurai:v3.

src/types.ts Outdated
* values are primarily decorated onto IncomingMessage by express, but are
* not required for use.
*/
declare module 'http' {
Copy link
Owner

Choose a reason for hiding this comment

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

Augmenting http.IncomingMessage does some assumptions on properties from certain frameworks; ie. express.

Wondering if there is a way to provide better support (without hardcoding them?)

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 AFK for a day or so, but iirc, HPM mutates and/or attempts to read these values. This augment was only to satisfy HPMs existing internal usage of these fields. It does not require frameworks to implement these, luckily.

Will have to visit on a field by field basis. Probably worth going thru them and seeing if we can avoid

Copy link
Author

@cdaringe cdaringe Mar 13, 2022

Choose a reason for hiding this comment

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

Alright, I had some time this morning.

I was almost able to decouple full from express, and improve typesafety (by reducing casts).

cdaringe#1

Unfortunately, there is one macro barrier--and that is that fact that internally express mutates req.url in a fn called trim_prefix and exposes the desired url on originalUrl. this is harmful behavior, and make supporting express difficult on a plain node http, particularly because apps can app.use(...) at many levels inside of express.

so, we need to either need to augment http.IncomingMessage as done above and continue to support originalUrl, or, update the HPM createProxyMiddleware inferface & accept a pathPrefix (🤢 ) and use that to reconstruct original req.urls. The latter sounds dicey, and perhaps has edge cases.

Copy link
Author

Choose a reason for hiding this comment

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

I also posted to express here: expressjs/express#4854

Copy link
Owner

@chimurai chimurai Mar 13, 2022

Choose a reason for hiding this comment

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

Did some testing today.

Think augmenting native types is somewhat undesirable; As these augmented types will bubble up to the projects in which http-proxy-middleware is used.

Wouldn't adding back @types/express as devDependency makes sense? And cast internal req objects to express.Request at places where it's needed. Instead of creating and maintaining them as internal types. This way it's also documented for which framework it's serving its purpose.

Copy link
Author

Choose a reason for hiding this comment

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

Think augmenting native types is somewhat undesirable; As these augmented types will bubble up to the projects in which http-proxy-middleware is used.

Agreed. Let's not do that.

Wouldn't adding back @types/express as devDependency makes sense? And cast internal req objects to express.Request at places where it's needed.

Ya, we can do that

Copy link
Author

Choose a reason for hiding this comment

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

mmm, in retrospect, if we bring them back and cast, they'd need to be a peer dependency or top level dependency for TS users who aren't skipLibCheck: true'ing :).

the only fields that are needed are body & originalUrl--pretty minimal. to prevent users from having to deal with library and dependency hoopla, i was able to put in minimal types, right around the runtime places where we need to handle express.js special cases as well. thus, we can co-locate express compile & runtime special cases simultaneously, adjacent to one another, with no dependency complexity.

let me annotate my new patches to draw attention to this to you--see if you're game

Copy link
Owner

Choose a reason for hiding this comment

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

Getting into a rabbit-hole... Didn't expect changes/refactoring of the internals. 😬
Need some time to review and see if it's going into the right direction.
It's a tough cookie...

Copy link
Owner

Choose a reason for hiding this comment

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

Getting into a rabbit-hole... Didn't expect changes/refactoring of the internals. 😬
Need some time to review and see if it's going into the right direction.
It's a tough cookie...

Copy link
Author

@cdaringe cdaringe Mar 14, 2022

Choose a reason for hiding this comment

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

Didn't expect changes/refactoring of the internals

ya... sorry about that. it's much less evil than it looks! mainly just a single param drop and halt of req.url usage (both simple!), which generated a ton of 🔴 / 🟢 in the diffs. nonetheless, it was all still on topic for de-expressing--promise! 😬 🙏

jest.setup.js Show resolved Hide resolved
import type * as httpProxy from 'http-proxy';
import { getInstance } from './logger';
import { getUrl } from './url';
Copy link
Author

@cdaringe cdaringe Mar 13, 2022

Choose a reason for hiding this comment

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

👀 as mentioned in issue discussions, we now have clarity that we cannot just use req.url--it's mutated from express. thus, from the Request type, I have now dropped url: string, as it's unsafe to access in the codebase. getUrl is a safe, express & IncomingMessage compatible accessor

export function fixRequestBody(proxyReq: http.ClientRequest, req: http.IncomingMessage): void {
const requestBody = (req as Request).body;
export function fixRequestBody(proxyReq: http.ClientRequest, req: Request): void {
const requestBody = (req as BodyParserRequest).body;
Copy link
Author

Choose a reason for hiding this comment

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

here's an express runtime workaround, with an express type cast, all localized to the module of interest

return pathFilter(pathname, req);
export function matchPathFilter(pathFilter: Filter = '/', req: Request): boolean {
const url = getUrl(req);
switch (typeof pathFilter) {
Copy link
Author

Choose a reason for hiding this comment

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

it's not that I love switch vs the prior ifs, it's rather that the switch narrows pathFilter's many types into narrow types in each block

*
* Use getUrl(req) to get a x-server safe URL.
*/
type ExpressCompatibleIncomingMessage = Omit<http.IncomingMessage, 'url'>;
Copy link
Author

Choose a reason for hiding this comment

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

👀 . Here's a big change since our last review. Dropping url out of IncomingMessage, as it's unsafe to reference anywhere due to express support

if (typeof pathFilter === 'function') {
const pathname = getUrlPathName(uri);
return pathFilter(pathname, req);
export function matchPathFilter(pathFilter: Filter = '/', req: Request): boolean {
Copy link
Author

Choose a reason for hiding this comment

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

this is the 2nd biggest change with this PR since we last reviewed. uri: string was dropped as a param. now, url is always extract from the req param, safely.

a nice little bit of simplification 🧹 . this was done as part of the de-express-ificationthe

@cdaringe
Copy link
Author

cdaringe commented Mar 15, 2022

Perhaps for a different issue, but directly related to this PR's refactors.

I was speaking w/ dougwilson over in express, and he made a pretty good callout here, that use of originalUrl is likely entirely incorrect in HPM.

That is, HPM shouldn't handle the express special pathname case(s) it handles now at all.

Currently, HPM+express does this:

app.use('/api', proxy('http://localhost/')) 
// then, GET /api/foo => http://localhost/api/foo

but it actually should be:

app.use('/api', proxy('http://localhost/')) 
// then, GET /api/foo => http://localhost/foo

If someone wants the original behavior, then they needed to register the HPM middleware earlier in their app middlewares, and not in the nested app mount. If they wanted something different, then they'd need to use one of HPMs many awesome rewrite APIs.

I think for v3, we ought take heed of this warning (from the express maintainer, no less 😄 ). It additionally simplifies a bunch of the tomfoolery i've done here!

If agreeable, I'd refactor everything accordingly, possibly in a downstream PR, or here.

@chimurai
Copy link
Owner

The originalUrl you've found is exactly the rabbit hole, which I was referring to previously. (You're the first to spot it 😅)
This faulty implementation should be completely removed. (Good to know /dougwilson confirmed the incorrect usage. Thanks for reaching out to him)

Think too many things are happening in #723.

To make it more manageable I would suggest to break it apart:

  1. provide better x-server compatibility
  2. plugin system
  3. move functionality to plugin system (pathRewrite)
  4. remove originalUrl usage and express types?

These were actually things I already started to work on in v3 to allow better compatibility and flexibility.

To provide better x-server compatibility; I created a new branch #730 and cherry picked the changes you made with the focus on improving the base types so you can use in native http server or other express-like servers. (Added you as co-author 🙏). Let me know if this suites your original need in #719.

After that I can continue to work on the plugin system. And clean up the internals. (should make removing originalUrl bit easier)
Bit unfortunate timing... If you start refactoring now, I'm afraid we'll get a lot of conflicts 😬

Removing originalUrl will change how you'll create and mount the proxy, configure it, pathRewriting. But also how can you mount it to sub-routes. Think how WebSockets will be proxied needs to be investigated. This seemingly tiny change will need quite some testing...

About:

This is correct.

app.use('/api', proxy('http://localhost/')) 
// then, GET /api/foo => http://localhost/foo

Without originalUrl changing the basePath will be possible as well with:

app.use('/api', proxy('http://localhost/bar')) 
// then, GET /api/foo => http://localhost/bar/foo

@cdaringe
Copy link
Author

I agree, there's lot's going on. It is entirely focused on 1 & 4, sans the jest patch. I'm picking up that you have some stuff in flight you wanna do w/ v3, that perhaps intersects with this work? That's fine. I'm probably at this point not going to send four PRs as you've suggested above. for me, i'd vote for one of the the following:

  1. Option 1: we close this PR, and it can just sit here as reference as you noodle thru your v3 plans. i feel like the types are in a good position as they stand here. additionally, as mentioned earlier, some of the thrash is really just to sustain backwards compat w/ expreess stuff and still have good typescript hygiene. with the deletion of that feature, a lot of that diff thrash disappears out of refactor: use base node http types #723
  2. Option 2: accept this PR, and iterate on those other goals. i'm happy to even jump on a call to walk thru this changeset together. v3 is unreleased at this point, so perhaps there's little harm in taking a slightly above average sized changeset.

i wont be upset if option #1 is the pick :)

@chimurai
Copy link
Owner

You sensed it right. Had some plans in mind related to #2 and #3, which hopefully will answers a lot of issues and questions in the issue tracker. Since it's a major version, it's a nice opportunity to do some clean up as well.

If you don't mind, I'll continue with option #1 to keep the changes small and logical and create separate PRs to remove incorrect usage of originalUrl and maybe remove the @express/types dependency.

Really appreciate the time and effort you've put into it. 🙏
Want to thank you for helping out with the improvements to the types and more. (Was struggling to get the types right)
Wish there were more developers like you who aren't afraid to get their hands dirty. :)

@chimurai
Copy link
Owner

Closing PR. Superseded by #730

Will create separated PR to fix the originalUrl issue (#723 (comment)) and investigate removal of the @types/express dependency.

Thanks for helping out! 💪

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.

3 participants