microsoft/playwright · error · Error

cert is specified without key

Error message

cert is specified without key

What it means

Thrown by verifyClientCertificates() when a clientCertificates entry has cert set but key is falsy. X.509 client auth requires both halves of the key pair, so Playwright rejects an asymmetric entry before building the TLS secure context.

Source

Thrown at packages/playwright-core/src/server/browserContext.ts:804

  const { longitude, latitude, accuracy } = geolocation;
  if (longitude < -180 || longitude > 180)
    throw new Error(`geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.`);
  if (latitude < -90 || latitude > 90)
    throw new Error(`geolocation.latitude: precondition -90 <= LATITUDE <= 90 failed.`);
  if (accuracy < 0)
    throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
}

export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) {
  if (!clientCertificates)
    return;
  for (const cert of clientCertificates) {
    if (!cert.origin)
      throw new Error(`clientCertificates.origin is required`);
    if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
      throw new Error('None of cert, key, passphrase or pfx is specified');
    if (cert.cert && !cert.key)
      throw new Error('cert is specified without key');
    if (!cert.cert && cert.key)
      throw new Error('key is specified without cert');
    if (cert.pfx && (cert.cert || cert.key))
      throw new Error('pfx is specified together with cert, key or passphrase');
  }
}

export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings {
  let { server, bypass } = proxy;
  let url;
  try {
    // new URL('127.0.0.1:8080') throws
    // new URL('localhost:8080') fails to parse host or protocol
    // In both of these cases, we need to try re-parse URL with `http://` prefix.
    url = new URL(server);
    if (!url.host || !url.protocol)
      url = new URL('http://' + server);
  } catch (e) {

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Supply key alongside cert (key: fs.readFileSync('./client.key') or keyPath resolved correctly).
  2. If you only have a bundled file, split it into cert and key, or use the pfx form instead and remove cert/key.
  3. Double-check that keyPath points to an existing readable file so the Buffer is populated before validation.

Example fix

// before
clientCertificates: [{ origin, cert: fs.readFileSync('client.crt') }]
// after
clientCertificates: [{ origin, cert: fs.readFileSync('client.crt'), key: fs.readFileSync('client.key') }]
Defensive patterns

Strategy: validation

Validate before calling

function validateCertKeyPair(certs?: ClientCertificate[]) {
  for (const c of certs || []) {
    if (c.cert && !c.key) throw new Error(`cert set without key for ${c.origin}`);
  }
}

Type guard

function isCompleteCertKeyPair(c: ClientCertificate): boolean {
  return (!!c.cert && !!c.key) || (!c.cert && !c.key);
}

Prevention

When it happens

Trigger: browser.newContext({ clientCertificates: [{ origin, cert: <Buffer>, /* key missing */ }] }) or the equivalent via APIRequestContext.newContext / launchPersistentContext.

Common situations: Developer pastes the certificate buffer but forgets the private key, or sets keyPath to a wrong/empty path that resolved to undefined.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/d0e4149fb0705020. Report an issue: GitHub.