denoland/deno · error · TypeError

ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS

ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS

Error message

The ALPNCallback and ALPNProtocols TLS options are mutually exclusive

What it means

TLS ALPN can be configured exactly one way: a static ALPNProtocols list, or an ALPNCallback invoked per handshake — never both. initializeTLSOptions() in the http2 polyfill enforces the same rule as Node: if options.ALPNCallback is truthy while options.ALPNProtocols is defined, it throws ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS.

Source

Thrown at ext/node/polyfills/http2.ts:5085

  // Used only with allowHTTP1
  const http1Options = options.http1Options ?? {};
  options.Http1IncomingMessage ||= http1Options.IncomingMessage ||
    http.IncomingMessage;
  options.Http1ServerResponse ||= http1Options.ServerResponse ||
    http.ServerResponse;

  options.Http2ServerRequest ||= Http2ServerRequest;
  options.Http2ServerResponse ||= Http2ServerResponse;
  return options;
}

function initializeTLSOptions(options, servername) {
  options = initializeOptions(options);

  if (options.ALPNCallback) {
    if (options.ALPNProtocols !== undefined) {
      throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
    }
    // rustls does not expose a per-handshake ALPN selection callback, so
    // we approximate it by pre-evaluating the user's ALPNCallback once
    // and advertising the returned protocol. This only correctly handles
    // callbacks that synchronously return a fixed protocol string; richer
    // selection (rejecting handshakes via false/undefined, choosing per
    // client-offered protocols) is not supported, so reject those shapes
    // up-front rather than silently advertising an empty ALPN list.
    const selected = options.ALPNCallback({ servername, protocols: [] });
    if (typeof selected !== "string" || selected.length === 0) {
      throw new ERR_INVALID_ARG_VALUE(
        "options.ALPNCallback",
        selected,
        "must synchronously return a non-empty protocol string; " +
          "dynamic per-handshake ALPN selection is not supported",
      );
    }
    options.ALPNProtocols = [selected];

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Keep exactly one of the two options: delete ALPNProtocols if you need the callback, otherwise delete ALPNCallback.
  2. When merging config sources, explicitly remove the stale key (delete merged.ALPNProtocols or delete merged.ALPNCallback) before passing to createSecureServer.
  3. In Deno prefer ALPNProtocols — dynamic ALPNCallback behavior is additionally restricted there (see the ERR_INVALID_ARG_VALUE error on ALPNCallback).

Example fix

// before
const server = http2.createSecureServer({
  key, cert,
  ALPNProtocols: ['h2', 'http/1.1'],
  ALPNCallback: (protos) => protos.includes('h2') ? 'h2 : false,
});

// after
const server = http2.createSecureServer({
  key, cert,
  ALPNProtocols: ['h2', 'http/1.1'],
  allowHTTP1: true,
});
Defensive patterns

Strategy: validation

Validate before calling

function assertExclusiveALPN(options) {
  if (options?.ALPNCallback && options?.ALPNProtocols !== undefined) {
    throw new Error('Use either ALPNCallback or ALPNProtocols, not both');
  }
}
assertExclusiveALPN(tlsOptions);
http2.createSecureServer(tlsOptions);

Type guard

function hasSingleALPNOption(o) {
  return !(o && o.ALPNCallback && o.ALPNProtocols !== undefined);
}

Prevention

When it happens

Trigger: http2.createSecureServer() or http2.connect() with an options object containing both keys, e.g. { key, cert, ALPNProtocols: ['h2', 'http/1.1'], ALPNCallback: (protos) => 'h2' }. Also happens when TLS options are spread from an existing https-server config that already set ALPNProtocols and code then adds ALPNCallback.

Common situations: Merging a shared TLS options object (which already has ALPNProtocols) with code that adds ALPNCallback; upgrading a server from a static list to a callback without deleting the old key; copy-pasted TLS snippets combining both.

Understand the failure class

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/5cba7e96ad98019e. Report an issue: GitHub.