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

  1. Use the matching module's agent: https.globalAgent / new https.Agent() for https requests
  2. Keep one agent per scheme and select by protocol: const agent = protocol === 'https:' ? httpsAgent : httpAgent
  3. 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

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)