-
Notifications
You must be signed in to change notification settings - Fork 228
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
Fix keepAlive
for http-proxy-agent
#169
Fix keepAlive
for http-proxy-agent
#169
Conversation
* All existing keepAlive tests now assert OK status * Added http-proxy-agent keepAlive test * Added pac-proxy-agent keepAlive tests * Added proxy-agent keepAlive test and cache tests
🦋 Changeset detectedLatest commit: a841e14 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
14b1490
to
a546c05
Compare
addRequest(req: InternalClientRequest, opts: AgentConnectOpts): void { | ||
this.setRequestProps(req, opts); | ||
// @ts-expect-error `addRequest()` isn't defined in `@types/node` | ||
super.addRequest(req, opts); | ||
} | ||
|
||
async connect( | ||
req: InternalClientRequest, | ||
opts: AgentConnectOpts | ||
): Promise<net.Socket> { | ||
if (this.setRequestPropsOnConnect) { | ||
this.setRequestProps(req, opts); | ||
} | ||
// Create a socket connection to the proxy server. | ||
let socket: net.Socket; |
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.
This is the primary change to fix the problem. I noticed that when keepAlive
is enabled, the call stack for the first request is Agent.addRequest()
-> Agent.createSocket()
-> Agent.connect()
, and all subsequent requests only call Agent.addRequest()
.
- Before:
HttpProxyAgent.connect()
rewrites the path and add headers for only the first request - After:
- In "http-proxy-agent":
HttpProxyAgent.addRequest()
will rewrite the path and add headers for all requests - In "proxy-agent" and "pac-proxy-agent": we need to use the
setRequestPropsOnConnect
option to causeHttpProxyAgent.connect()
to rewrite the path and add headers for the first request, and then we have the right context to rewrite the path and add headers for all subsequent requests.
- In "http-proxy-agent":
It's not ideal to have the second code path with the setRequestPropsOnConnect
option, but because of the way that "proxy-agent" and "pac-proxy-agent" abstract out the usage of "http-proxy-agent", I couldn't see a cleaner way to do this.
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 tweaked this in 2ddd92f in order to descope this PR and maintain full backwards compatibility with ProxyAgent/PacProxyAgent, see #169 (comment)
packages/proxy-agent/src/index.ts
Outdated
@@ -41,7 +39,7 @@ export const proxies: { | |||
'pac-ftp': [PacProxyAgent, PacProxyAgent], | |||
'pac-http': [PacProxyAgent, PacProxyAgent], | |||
'pac-https': [PacProxyAgent, PacProxyAgent], | |||
}; | |||
} satisfies { [P in ValidProtocol]: [AgentConstructor, AgentConstructor] }; |
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.
Using the satisfies
operator introduced in TS 4.9 allows us to preserve more specific type information on the proxies
object. That means that // @ts-expect-error meh…
below is no longer necessary, I was able to remove 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.
Nice! This may constitute a breaking change though. If we're relying on any TS 4.9+ features already then that wouldn't be the case.
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.
Oh interesting, I hadn't thought of that, just assumed it would be OK because this repo uses TS 5.0.
I just checked the contents of packages/proxy-agent/dist/index.d.ts
, and it looks like satisfies
operator has been stripped out of the compiled type definition:
/**
* Supported proxy types.
*/
export declare const proxies: {
http: [typeof HttpProxyAgent, typeof HttpsProxyAgent];
https: [typeof HttpProxyAgent, typeof HttpsProxyAgent];
socks: [typeof SocksProxyAgent, typeof SocksProxyAgent];
socks4: [typeof SocksProxyAgent, typeof SocksProxyAgent];
socks4a: [typeof SocksProxyAgent, typeof SocksProxyAgent];
socks5: [typeof SocksProxyAgent, typeof SocksProxyAgent];
socks5h: [typeof SocksProxyAgent, typeof SocksProxyAgent];
'pac-data': [typeof PacProxyAgent, typeof PacProxyAgent];
'pac-file': [typeof PacProxyAgent, typeof PacProxyAgent];
'pac-ftp': [typeof PacProxyAgent, typeof PacProxyAgent];
'pac-http': [typeof PacProxyAgent, typeof PacProxyAgent];
'pac-https': [typeof PacProxyAgent, typeof PacProxyAgent];
};
I guess operators like this are just used at compile time. TIL!
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.
This test was fixed in 6f3bc0d. I forgot to remove the comment in that commit.
Thank you for identifying the primary bug in
|
Sure thing, I'm glad I can contribute!
Yeah... I set out to do one thing and it ballooned a bit. I'm happy to split this up, I'll take a crack at it tomorrow morning. |
const base = `${protocol}//${hostname}`; | ||
const base = `http://${hostname}`; |
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 seemed redundant to have const protocol = opts.secureEndpoint ? 'https:' : 'http:';
and use that to generate the URL, when HttpProxyAgent should only be used for HTTP URLs. In any case, the secureEndpoint
option is always undefined Agent.addRequest()
(except for when ProxyAgent is used).
I simplified it here by hard-coding the protocol, I think that's OK.
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.
If we're going to do this, then the function should throw an error when opts.secureEndpoint === true
. However, I think it is valid to do HTTPS requests (though less ideal, especially over an external network). I believe squid works when passing an https URL for example.
addRequest(req: HttpProxyAgentClientRequest, opts: AgentConnectOpts): void { | ||
this.setRequestProps(req, opts); | ||
// @ts-expect-error `addRequest()` isn't defined in `@types/node` | ||
super.addRequest(req, opts); | ||
} | ||
|
||
async connect( | ||
req: HttpProxyAgentClientRequest, | ||
opts: AgentConnectOpts | ||
): Promise<net.Socket> { | ||
if (!req._updatedPropsForProxyAgent) { | ||
// Maintain backwards compatibility with ProxyAgent and | ||
// PacProxyAgent, which both rely on HttpProxyAgent to set request | ||
// props inside the connect() function. | ||
this.setRequestProps(req, opts); | ||
} |
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 changed this to work in a fully backwards-compatible way.
ProxyAgent and PacProxyAgent still function and all tests pass.
req._implicitHeader(); | ||
if (!req._header) { | ||
req._implicitHeader(); | ||
} |
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 had to add this conditional because now that we are setting the headers before addRequest()
, it causes req._header
to be non-null here, which will cause req._implicitHeader()
to throw an exception.
Alternatively, I could do just allow connect()
to rewrite the request URL and set headers, even if it's already happened in addRequest()
:
diff --git a/packages/http-proxy-agent/src/index.ts b/packages/http-proxy-agent/src/index.ts
index 16d5b51..807e4c5 100644
--- a/packages/http-proxy-agent/src/index.ts
+++ b/packages/http-proxy-agent/src/index.ts
@@ -29,7 +29,6 @@ interface HttpProxyAgentClientRequest extends http.ClientRequest {
}[];
_header?: string | null;
_implicitHeader(): void;
- _updatedPropsForProxyAgent?: boolean;
}
function isHTTPS(protocol?: string | null): boolean {
@@ -104,8 +103,6 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
this.keepAlive ? 'Keep-Alive' : 'close'
);
}
-
- req._updatedPropsForProxyAgent = true;
}
addRequest(req: HttpProxyAgentClientRequest, opts: AgentConnectOpts): void {
@@ -118,12 +115,10 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
req: HttpProxyAgentClientRequest,
opts: AgentConnectOpts
): Promise<net.Socket> {
- if (!req._updatedPropsForProxyAgent) {
- // Maintain backwards compatibility with ProxyAgent and
- // PacProxyAgent, which both rely on HttpProxyAgent to set request
- // props inside the connect() function.
- this.setRequestProps(req, opts);
- }
+ // Maintain backwards compatibility with ProxyAgent and
+ // PacProxyAgent, which both rely on HttpProxyAgent to set request
+ // props inside the connect() function.
+ this.setRequestProps(req, opts);
// Create a socket connection to the proxy server.
let socket: net.Socket;
@@ -141,9 +136,7 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
let first: string;
let endOfHeaders: number;
debug('Regenerating stored HTTP header string for request');
- if (!req._header) {
- req._implicitHeader();
- }
+ req._implicitHeader();
if (req.outputData && req.outputData.length > 0) {
// Node >= 12
debug(
What do you think?
agent, | ||
}); | ||
expect(res2.headers.connection).toEqual('keep-alive'); | ||
// expect(res2.statusCode).toEqual(200); // TODO: this assertion will cause this test to fail; uncomment in a follow-on PR to fix keepAlive for PacProxyAgent+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 left the new tests and test assertions intact.
I simply commented out the assertions that are currently failing.
OK, I descoped this PR to focus on fixing HttpProxyAgent as requested, this is ready for review whenever you have time. |
Based on #169. Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
Thanks, looks good to me! I like your approach of checking the path instead of setting a flag on the request object. |
Looking forward to any more follow-up PRs from you as well 😃 |
I was testing the new
keepAlive
option, and it turns out that while it works for https-proxy-agent, it doesn't work with http-proxy-agent.I use Smokescreen, and when testing keepAlive with http-proxy-agent, the first request succeeded, but all subsequent requests failed with an error: "This is a proxy server. Does not respond to non-proxy requests"
Turns out, the first request used an absolute URL (
http://localhost:3000/foo
) while all subsequent requests used relative URLs (/foo
). HTTP proxy servers require that HTTP requests use absolute URLs, which is why this happens. (HTTPS requests are not impacted because they are encrypted, and regular non-MITM proxies cannot read the URL).The http-proxy-agent package already had logic to rewrite the relative URL to an absolute URL:
proxy-agents/packages/http-proxy-agent/src/index.ts
Lines 88 to 90 in 2f835a4
However, that only happens on
connect()
, so it only worked on the first request. This PR makes the following changes: