Latent mTLS trap — the getHttpsAgent() pattern in vuer_oss is a silent no-op

Every fetch() call in vuer_oss passes a node-fetch-style agent: new https.Agent({...}), but these are Node’s global fetch (undici-based). Global fetch ignores agent entirely — it only honors dispatcher. The pattern is harmless today only because cv.rejectUnauthorized defaults to true (same as global fetch’s default). Any real attempt to send client certificates (mTLS) or to disable verification via agent is silently dropped.

Discovered while scoping FKITDEV-8947 (migrate the UniCredit customization service off request-promise-native to fetch). All facts below were empirically verified on Node v22.22.3 (bundled undici 6.24.1).

The trap

In vuer_oss, the getHttpsAgent() idiom builds an https.Agent and passes it to fetch as agent. Call sites include:

  • server/cv/visions/*/*Api.js
  • server/service/VuerCVService.js
  • server/ocr/engines/*
  • server/faceRecognition/engine/VuerCVFaceRecognitionEngine.js
  • customization/api/sms/SmsApiSmsApiCom.js

For Agents

Neither undici nor node-fetch is a declared dependency of vuer_oss. Therefore every one of these fetch() calls resolves to Node’s global fetch (undici under the hood), and the agent: option is meaningless to it.

Verified facts

  1. Global fetch ignores agent. Node’s global (undici) fetch honors only dispatcher, never the node-fetch-style agent. Proof: fetch(selfSignedUrl, { agent: new https.Agent({ rejectUnauthorized: false }) }) FAILED with TypeError: fetch failedcause: DEPTH_ZERO_SELF_SIGNED_CERT. The agent’s rejectUnauthorized: false was not applied.

  2. So getHttpsAgent() is a silent no-op. It works only by accident: cv.rejectUnauthorized defaults to true, which is identical to global fetch’s default behavior. Disabling verification, or supplying a client cert/key (mTLS), through agent does nothing.

  3. The fix is to use the undici package’s own fetch + Agent together (version-matched within the package):

    const { fetch, Agent } = require('undici')
     
    const r = await fetch(url, {
      dispatcher: new Agent({
        connect: { cert, key, ca, passphrase, rejectUnauthorized }
      })
    })

    Proof: against a local mTLS server (requestCert), this returned status 200 and the server confirmed it received the client certificate (peer subject CN present).

Cross-version gotcha (why undici’s own fetch, not a standalone Agent)

Do NOT feed a standalone-undici Agent into Node's GLOBAL fetch

Mixing Node’s global fetch (bundled undici 6.24.1) with a dispatcher from a different-major standalone undici (tested 8.5.0) FAILS with UND_ERR_INVALID_ARG: invalid onRequestStart method — a handler-interface mismatch between undici majors.

Because Facekom deploys on both Node 22 and Node 24 (which bundle different undici majors), pinning a standalone undici to “exactly the bundled version” is fragile and breaks across the 22/24 split. Use the undici package’s own fetch paired with its own Agent — that pair is self-consistent regardless of which undici Node bundles.

Why this matters for FKITDEV-8947

The ticket migrates customization/server/service/UniCredit/ApiService.js (branch customization/unicredit, class UniCreditApiService) off request-promise-native. That service uses mTLScert / key / ca / passphrase merged from the portal.api config — so a naive agent:-style migration would silently break its TLS (client cert never sent). It must use the undici.fetch + undici.Agent pattern above.

Minor, ticket-specific (not part of the gotcha)

The working tree of that file currently has an uncommitted stray }w typo at line 267 (HEAD is clean) causing a ReferenceError in the non-realtime-sdk branch of setIdentificationComplete.

  • FKITDEV-8947 — the scoping work that surfaced this; UniCredit request-promise-native → fetch migration (mTLS).
  • vuer_oss — the service whose getHttpsAgent() pattern is the silent no-op.