Skip to content

Commit 66b4c63

Browse files
authored
[agent-base] Allow for never relying on stack trace (#170)
During an HTTP request, if `opts.protocol` is set, then use that to determine the `secureEndpoint` value. Coupling that with an explicitly set `agent.protocol` in the instance (essentially "locking" it for use with either `http` or `https` module), then the agent will _never_ rely on the stack trace. By default it will still rely on the stack trace to allow for agent instances to be used with either `http` or `https` modules. Also moved the internal props to an object using a symbol, instead of relying on underscore props. Fixes #158.
1 parent 2f835a4 commit 66b4c63

File tree

4 files changed

+100
-32
lines changed

4 files changed

+100
-32
lines changed

.changeset/seven-horses-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agent-base": patch
3+
---
4+
5+
Allow for never relying on stack trace

packages/agent-base/src/helpers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ export function req(
3333
url: string | URL,
3434
opts: https.RequestOptions = {}
3535
): ThenableRequest {
36-
let req!: ThenableRequest;
36+
const href = typeof url === 'string' ? url : url.href;
37+
const req = (href.startsWith('https:') ? https : http).request(
38+
url,
39+
opts
40+
) as ThenableRequest;
3741
const promise = new Promise<http.IncomingMessage>((resolve, reject) => {
38-
const href = typeof url === 'string' ? url : url.href;
39-
req = (href.startsWith('https:') ? https : http)
40-
.request(url, opts, resolve)
42+
req
43+
.once('response', resolve)
4144
.once('error', reject)
4245
.end() as ThenableRequest;
4346
});

packages/agent-base/src/index.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,35 @@ function isSecureEndpoint(): boolean {
1919

2020
interface HttpConnectOpts extends net.TcpNetConnectOpts {
2121
secureEndpoint: false;
22+
protocol?: string;
2223
}
2324

2425
interface HttpsConnectOpts extends tls.ConnectionOptions {
25-
port: number;
2626
secureEndpoint: true;
27+
protocol?: string;
28+
port: number;
2729
}
2830

2931
export type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts;
3032

33+
const INTERNAL = Symbol('AgentBaseInternalState');
34+
35+
interface InternalState {
36+
defaultPort?: number;
37+
protocol?: string;
38+
currentSocket?: Duplex;
39+
}
40+
3141
export abstract class Agent extends http.Agent {
32-
_defaultPort?: number;
33-
_protocol?: string;
34-
_currentSocket?: Duplex;
42+
private [INTERNAL]: InternalState;
3543

3644
// Set by `http.Agent` - missing from `@types/node`
3745
options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;
3846
keepAlive!: boolean;
3947

4048
constructor(opts?: http.AgentOptions) {
4149
super(opts);
42-
this._defaultPort = undefined;
43-
this._protocol = undefined;
50+
this[INTERNAL] = {};
4451
}
4552

4653
abstract connect(
@@ -53,51 +60,79 @@ export abstract class Agent extends http.Agent {
5360
options: AgentConnectOpts,
5461
cb: (err: Error | null, s?: Duplex) => void
5562
) {
56-
const o = {
57-
...options,
58-
secureEndpoint: options.secureEndpoint ?? isSecureEndpoint(),
59-
};
63+
// Need to determine whether this is an `http` or `https` request.
64+
// First check the `secureEndpoint` property explicitly, since this
65+
// means that a parent `Agent` is "passing through" to this instance.
66+
let secureEndpoint =
67+
typeof options.secureEndpoint === 'boolean'
68+
? options.secureEndpoint
69+
: undefined;
70+
71+
// If no explicit `secure` endpoint, check if `protocol` property is
72+
// set. This will usually be the case since using a full string URL
73+
// or `URL` instance should be the most common case.
74+
if (
75+
typeof secureEndpoint === 'undefined' &&
76+
typeof options.protocol === 'string'
77+
) {
78+
secureEndpoint = options.protocol === 'https:';
79+
}
80+
81+
// Finally, if no `protocol` property was set, then fall back to
82+
// checking the stack trace of the current call stack, and try to
83+
// detect the "https" module.
84+
if (typeof secureEndpoint === 'undefined') {
85+
secureEndpoint = isSecureEndpoint();
86+
}
87+
88+
const connectOpts = { ...options, secureEndpoint };
6089
Promise.resolve()
61-
.then(() => this.connect(req, o))
90+
.then(() => this.connect(req, connectOpts))
6291
.then((socket) => {
6392
if (socket instanceof http.Agent) {
6493
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
65-
return socket.addRequest(req, o);
94+
return socket.addRequest(req, connectOpts);
6695
}
67-
this._currentSocket = socket;
96+
this[INTERNAL].currentSocket = socket;
6897
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
6998
super.createSocket(req, options, cb);
7099
}, cb);
71100
}
72101

73102
createConnection(): Duplex {
74-
if (!this._currentSocket) {
75-
throw new Error('no socket');
103+
const socket = this[INTERNAL].currentSocket;
104+
this[INTERNAL].currentSocket = undefined;
105+
if (!socket) {
106+
throw new Error(
107+
'No socket was returned in the `connect()` function'
108+
);
76109
}
77-
return this._currentSocket;
110+
return socket;
78111
}
79112

80113
get defaultPort(): number {
81-
if (typeof this._defaultPort === 'number') {
82-
return this._defaultPort;
83-
}
84-
const port = this.protocol === 'https:' ? 443 : 80;
85-
return port;
114+
return (
115+
this[INTERNAL].defaultPort ??
116+
(this.protocol === 'https:' ? 443 : 80)
117+
);
86118
}
87119

88120
set defaultPort(v: number) {
89-
this._defaultPort = v;
121+
if (this[INTERNAL]) {
122+
this[INTERNAL].defaultPort = v;
123+
}
90124
}
91125

92126
get protocol(): string {
93-
if (typeof this._protocol === 'string') {
94-
return this._protocol;
95-
}
96-
const p = isSecureEndpoint() ? 'https:' : 'http:';
97-
return p;
127+
return (
128+
this[INTERNAL].protocol ??
129+
(isSecureEndpoint() ? 'https:' : 'http:')
130+
);
98131
}
99132

100133
set protocol(v: string) {
101-
this._protocol = v;
134+
if (this[INTERNAL]) {
135+
this[INTERNAL].protocol = v;
136+
}
102137
}
103138
}

packages/agent-base/test/test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ describe('Agent (TypeScript)', () => {
3838
assert(agent instanceof Agent);
3939
assert(agent instanceof MyAgent);
4040
});
41+
42+
it('should support explicit `protocol`', async () => {
43+
class MyAgent extends Agent {
44+
async connect() {
45+
return http.globalAgent;
46+
}
47+
}
48+
const agent = new MyAgent();
49+
50+
// Default checks stack trace
51+
expect(agent.protocol).toEqual('http:');
52+
53+
agent.protocol = 'other:';
54+
expect(agent.protocol).toEqual('other:');
55+
56+
// If we use this with `http.get()` then it should error
57+
let err: Error | undefined;
58+
try {
59+
req(`http://127.0.0.1/foo`, { agent });
60+
} catch (_err) {
61+
err = _err as Error;
62+
}
63+
assert(err);
64+
expect(err.message).toEqual('Protocol "http:" not supported. Expected "other:"');
65+
});
4166
});
4267

4368
describe('"http" module', () => {

0 commit comments

Comments
 (0)