denoland/deno · error · TypeError
ERR_INVALID_PROTOCOL
ERR_INVALID_PROTOCOL
Error message
Protocol "${protocol}" not supported. Expected "${expectedProtocol}" What it means
ClientRequest compares options.protocol (or the agent's protocol) against the selected agent's protocol and requires them to match: an https: request needs an agent whose protocol is 'https:', an http: request one with 'http:'. A mismatch throws ERR_INVALID_PROTOCOL before any socket is opened, because the agent would otherwise speak the wrong TLS/plain scheme.
Solutions
- Use the matching module's agent: https.globalAgent / new https.Agent() for https requests
- Keep one agent per scheme and select by protocol: const agent = protocol === 'https:' ? httpsAgent : httpAgent
- Drop the agent option entirely and let each module use its own default
Example fix
// before
https.request(url, { agent: http.globalAgent }, cb); // ERR_INVALID_PROTOCOL
// after
import http from 'node:http';
import https from 'node:https';
const agents = {
'http:': new http.Agent({ keepAlive: true }),
'https:': new https.Agent({ keepAlive: true }),
};
const u = new URL(url);
https.request(u, { agent: agents[u.protocol] }, cb); Defensive patterns
Strategy: validation
Validate before calling
import http from 'node:http';
import https from 'node:https';
const agents = {
'http:': new http.Agent({ keepAlive: true }),
'https:': new https.Agent({ keepAlive: true }),
};
function agentFor(protocol) {
const a = agents[protocol];
if (!a) throw new Error(`no agent for ${protocol}`);
return a;
} Type guard
const agentMatchesProtocol = (agent, protocol) => !agent || !agent.protocol || agent.protocol === protocol;
Try / catch
try { https.request(url, { agent }, cb); }
catch (err) {
if (err.code === 'ERR_INVALID_PROTOCOL') {
return https.request(url, { agent: https.globalAgent }, cb);
}
throw err;
} Prevention
- Keep one agent per scheme; select it from the URL's protocol
- Never pass http.globalAgent to https.request or vice versa, including in library defaults
When it happens
Trigger: https.request(url, { agent: http.globalAgent }); http.request(url, { agent: new https.Agent() }); a custom Agent class whose default protocol disagrees with options.protocol; manually setting protocol: 'https:' while the default agent is the http one.
Common situations: Sharing one keepAlive agent instance across http and https modules in an app; global agent injection in libraries (axios-style defaults) forgetting scheme-specific agents; proxy agents created with http.Agent used for https targets.
Related errors
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/c92951a0bee61c90.
Report an issue: GitHub.
Appendix: source
Thrown at ext/node/polyfills/_http_client.js:567
);
}
this.agent = agent;
const protocol = options.protocol || defaultAgent.protocol;
let expectedProtocol = defaultAgent.protocol;
if (this.agent?.protocol) {
expectedProtocol = this.agent.protocol;
}
if (options.path) {
const path = String(options.path);
if (INVALID_PATH_REGEX.test(path)) {
throw new ERR_UNESCAPED_CHARACTERS("Request path");
}
}
if (protocol !== expectedProtocol) {
throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol);
}
const defaultPort = options.defaultPort ||
(this.agent?.defaultPort);
const optsWithoutSignal = { __proto__: null, ...options };
// The `_proxy*` fields are internal transport details set only by the proxy
// selection below. A caller must not be able to supply them directly: doing
// so would route the request through an arbitrary proxy while bypassing the
// target permission check that the proxy branch performs. Strip any that came
// in via `options` so only the values computed here are honored.
delete optsWithoutSignal._proxy;
delete optsWithoutSignal._proxyTargetHost;
delete optsWithoutSignal._proxyTargetPort;
delete optsWithoutSignal._proxyProtocol;
delete optsWithoutSignal._proxyUseProxyConnection;
View on GitHub (pinned to 9ad36f7a2c)