• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / devtools-shared / 16946613464

13 Aug 2025 06:56PM UTC coverage: 72.831% (+0.3%) from 72.518%
16946613464

push

github

web-flow
fix(devtools-connect): fix TLS error fallback mechanism MONGOSH-2488 (#566)

Restructure our proxy integration logic to:

- Avoid modifying the OIDC options – previously we were assigning to
  `clientOptions.oidc`, which meant that the `fetch` function created
  to read the system certificates was also used for the non-system-cert
  `connect()` call, breaking our fallback mechanism
- Add a test that verifies that the fallback mechanism works both for
  TLS driver connections and HTTPS OIDC calls.
- Deduplicate the logic for adding CA options to the `DevtoolsProxyOptions`
  instances.
- Share log info from the OIDC agent instance, if a new one has been
  created.
- Make sure that TLS errors passed through node-fetch are actually
  picked up by our "nested error" detection logic (they have a different
  `toStringTag` defined than regular `Error` instances).

1510 of 2359 branches covered (64.01%)

Branch coverage included in aggregate %.

18 of 30 new or added lines in 4 files covered. (60.0%)

35 existing lines in 2 files now uncovered.

3267 of 4200 relevant lines covered (77.79%)

594.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.52
/packages/devtools-proxy-support/src/agent.ts
1
import { ProxyAgent } from './proxy-agent';
1✔
2
import type { Agent as HTTPSAgent } from 'https';
3
import type { DevtoolsProxyOptions } from './proxy-options';
4
import { proxyForUrl } from './proxy-options';
1✔
5
import type { ClientRequest } from 'http';
6
import { Agent as HTTPAgent } from 'http';
1✔
7
import type { TcpNetConnectOpts } from 'net';
8
import type { ConnectionOptions, SecureContextOptions } from 'tls';
9
import type { Duplex } from 'stream';
10
import { SSHAgent } from './ssh';
1✔
11
import type { ProxyLogEmitter } from './logging';
12
import { EventEmitter } from 'events';
1✔
13
import type { AgentConnectOpts } from 'agent-base';
14
import { Agent as AgentBase } from 'agent-base';
1✔
15
import { mergeCA, systemCA } from './system-ca';
1✔
16

17
// Helper type that represents an https.Agent (= connection factory)
18
// with some custom properties that TS does not know about and/or
19
// that we add for our own purposes.
20
export type AgentWithInitialize = HTTPSAgent & {
21
  // This is genuinely custom for our usage (to allow establishing an SSH tunnel
22
  // first before starting to push connections through it)
23
  initialize?(): Promise<void>;
24
  logger?: ProxyLogEmitter;
25
  readonly proxyOptions?: Readonly<DevtoolsProxyOptions>;
26

27
  // This is just part of the regular Agent interface, used by Node.js itself,
28
  // but missing from @types/node
29
  createSocket(
30
    req: ClientRequest,
31
    options: TcpNetConnectOpts | ConnectionOptions,
32
    cb: (err: Error | null, s?: Duplex) => void,
33
  ): void;
34

35
  // http.Agent is an EventEmitter, just missing from @types/node
36
} & Partial<EventEmitter>;
37

38
class DevtoolsProxyAgent extends ProxyAgent implements AgentWithInitialize {
39
  readonly proxyOptions: DevtoolsProxyOptions;
40
  logger: ProxyLogEmitter;
41
  private sshAgent: SSHAgent | undefined;
42

43
  // Store the current ClientRequest for the time between connect() first
44
  // being called and the corresponding _getProxyForUrl() being called.
45
  // In practice, this is instantaneous, but that is not guaranteed by
46
  // the `ProxyAgent` API contract.
47
  // We use a Promise lock/mutex to avoid concurrent accesses.
48
  private _req: ClientRequest | undefined;
49
  private _reqLock: Promise<void> | undefined;
50
  private _reqLockResolve: (() => void) | undefined;
51

52
  // allowPartialTrustChain listed here until the Node.js types have it
53
  constructor(
54
    proxyOptions: DevtoolsProxyOptions & { allowPartialTrustChain?: boolean },
55
    logger: ProxyLogEmitter,
56
  ) {
57
    // NB: The Node.js HTTP agent implementation overrides request options
58
    // with agent options. Ideally, we'd want to merge them, but it seems like
59
    // there is little we can do about it at this point.
60
    // None of our products need the ability to specify per-request CA options
61
    // currently anyway.
62
    // https://github.com/nodejs/node/blob/014dad5953a632f44e668f9527f546c6e1bb8b86/lib/_http_agent.js#L239
63
    super({
39✔
64
      ...proxyOptions,
65
      getProxyForUrl: (url: string) => this._getProxyForUrl(url),
35✔
66
    });
67

68
    this.logger = logger;
39✔
69
    this.proxyOptions = proxyOptions;
39✔
70
    // This could be made a bit more flexible by actually dynamically picking
71
    // ssh vs. other proxy protocols as part of connect(), if we want that at some point.
72
    if (proxyOptions.proxy && new URL(proxyOptions.proxy).protocol === 'ssh:') {
39✔
73
      this.sshAgent = new SSHAgent(proxyOptions, logger);
5✔
74
    }
75
  }
76

77
  _getProxyForUrl = (url: string): string => {
39✔
78
    if (!this._reqLockResolve || !this._req) {
35!
79
      throw new Error('getProxyForUrl() called without pending request');
×
80
    }
81
    this._reqLockResolve();
35✔
82
    const req = this._req;
35✔
83
    this._req = undefined;
35✔
84
    this._reqLock = undefined;
35✔
85
    this._reqLockResolve = undefined;
35✔
86
    return proxyForUrl(this.proxyOptions, url, req);
35✔
87
  };
88

89
  async initialize(): Promise<void> {
90
    await this.sshAgent?.initialize();
19✔
91
  }
92

93
  override async connect(
94
    req: ClientRequest,
95
    opts: AgentConnectOpts & Partial<SecureContextOptions>,
96
  ): Promise<HTTPAgent> {
97
    opts.ca = mergeCA(this.proxyOptions.ca, opts.ca); // see constructor
40✔
98
    if (this.sshAgent) return this.sshAgent;
40✔
99
    while (this._reqLock) {
35✔
100
      await this._reqLock;
×
101
    }
102
    this._req = req;
35✔
103
    this._reqLock = new Promise((resolve) => (this._reqLockResolve = resolve));
35✔
104
    this.logger.emit('proxy:connect', { agent: this, req, opts });
35✔
105
    const agent = await super.connect(req, opts);
35✔
106
    // Work around https://github.com/TooTallNate/proxy-agents/pull/330
107
    if ('addRequest' in agent && typeof agent.addRequest === 'function') {
35!
108
      const dummyHttpAgent = Object.assign(new HTTPAgent(), {
35✔
109
        addRequest() {
110
          //ignore
111
        },
112
      });
113
      agent.addRequest(req, opts);
35✔
114
      return dummyHttpAgent;
34✔
115
    }
116
    return agent;
×
117
  }
118

119
  destroy(): void {
120
    this.sshAgent?.destroy();
31✔
121
    super.destroy();
31✔
122
  }
123
}
124

125
// Wraps DevtoolsProxyAgent with async CA resolution via systemCA()
126
class DevtoolsProxyAgentWithSystemCA extends AgentBase {
127
  readonly proxyOptions: DevtoolsProxyOptions;
128
  logger: ProxyLogEmitter = new EventEmitter();
39✔
129
  private agent: Promise<DevtoolsProxyAgent>;
130

131
  constructor(proxyOptions: DevtoolsProxyOptions) {
132
    super();
39✔
133
    this.proxyOptions = proxyOptions;
39✔
134
    this.agent = (async () => {
39✔
135
      const { ca } = await systemCA({
39✔
136
        ca: proxyOptions.ca,
137
        excludeSystemCerts: proxyOptions.caExcludeSystemCerts,
138
      });
139
      return new DevtoolsProxyAgent(
39✔
140
        { ...proxyOptions, ca, allowPartialTrustChain: true },
141
        this.logger,
142
      );
143
    })();
144
    this.agent.catch(() => {
39✔
145
      /* handled later */
146
    });
147
  }
148

149
  async initialize(): Promise<void> {
150
    const agent = await this.agent;
19✔
151
    await agent.initialize?.();
19✔
152
  }
153

154
  override async connect(): Promise<DevtoolsProxyAgent> {
155
    return await this.agent;
40✔
156
  }
157

158
  async destroy(): Promise<void> {
159
    (await this.agent).destroy();
31✔
160
  }
161
}
162

163
export function createAgent(
1✔
164
  proxyOptions: DevtoolsProxyOptions,
165
): AgentWithInitialize {
166
  return new DevtoolsProxyAgentWithSystemCA(proxyOptions);
39✔
167
}
168

169
export function useOrCreateAgent(
1✔
170
  proxyOptions: DevtoolsProxyOptions | AgentWithInitialize,
171
  target?: string,
172
  useTargetRegardlessOfExistingAgent = false,
20✔
173
): AgentWithInitialize | undefined {
174
  if (isExistingAgentInstance(proxyOptions)) {
27✔
175
    const agent = proxyOptions;
8✔
176
    if (
8!
177
      useTargetRegardlessOfExistingAgent &&
8!
178
      target !== undefined &&
179
      agent.proxyOptions &&
180
      !proxyForUrl(agent.proxyOptions, target)
181
    ) {
182
      return undefined;
×
183
    }
184
    return agent;
8✔
185
  } else {
186
    if (target !== undefined && !proxyForUrl(proxyOptions, target)) {
19!
UNCOV
187
      return undefined;
×
188
    }
189
    return createAgent(proxyOptions);
19✔
190
  }
191
}
192

193
export function isExistingAgentInstance(
1✔
194
  options: DevtoolsProxyOptions | AgentWithInitialize,
195
): options is AgentWithInitialize {
196
  return 'createConnection' in options;
27✔
197
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc