Latent mTLS trap — the
getHttpsAgent()pattern in vuer_oss is a silent no-opEvery
fetch()call invuer_osspasses a node-fetch-styleagent: new https.Agent({...}), but these are Node’s global fetch (undici-based). Global fetch ignoresagententirely — it only honorsdispatcher. The pattern is harmless today only becausecv.rejectUnauthorizeddefaults totrue(same as global fetch’s default). Any real attempt to send client certificates (mTLS) or to disable verification viaagentis 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.jsserver/service/VuerCVService.jsserver/ocr/engines/*server/faceRecognition/engine/VuerCVFaceRecognitionEngine.jscustomization/api/sms/SmsApiSmsApiCom.js
For Agents
Neither
undicinornode-fetchis a declared dependency of vuer_oss. Therefore every one of thesefetch()calls resolves to Node’s global fetch (undici under the hood), and theagent:option is meaningless to it.
Verified facts
-
Global fetch ignores
agent. Node’s global (undici) fetch honors onlydispatcher, never the node-fetch-styleagent. Proof:fetch(selfSignedUrl, { agent: new https.Agent({ rejectUnauthorized: false }) })FAILED withTypeError: fetch failed→cause: DEPTH_ZERO_SELF_SIGNED_CERT. The agent’srejectUnauthorized: falsewas not applied. -
So
getHttpsAgent()is a silent no-op. It works only by accident:cv.rejectUnauthorizeddefaults totrue, which is identical to global fetch’s default behavior. Disabling verification, or supplying a client cert/key (mTLS), throughagentdoes nothing. -
The fix is to use the
undicipackage’s ownfetch+Agenttogether (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
Agentinto Node's GLOBAL fetchMixing Node’s global fetch (bundled undici 6.24.1) with a
dispatcherfrom a different-major standalone undici (tested 8.5.0) FAILS withUND_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.
Implemented and proven — CIB, 2026-08-11
FKITDEV-9197 is the first round that actually shipped this pattern rather than scoping it. devel had deliberately removed request and request-promise-native from vuer_oss as a security retirement, so CIB’s customization REST clients had to be ported to fetch — including the Infocert mutual-TLS call sites.
undici ^6.28.0declared as a new direct dependency ofvuer_oss. It was already present transitively (viacheerio), so this is a declaration rather than new weight — but it is a new direct dep and is flagged as an open decision, reversible vianode:httpsif rejected.- Verified against a real client-certificate HTTPS server: 11/11 harness cases pass, including confirmed mTLS (
clientCN: "client"observed server-side).
The failure mode a naive port produces is silence
Two abandoned prior-art branches (
update/customization/cib-2025-11-12,update/customization/cib-2025-12-08) had already ported 4 files tofetch— and silently droppedagentOptions, which is howInfocertRestAPIpasses{ pfx, passphrase }at 6 sites. That port compiles, lints, and sends no client certificate. Nothing fails until the far end rejects the handshake.The same branches’ multipart port could not run at all:
formData.getHeaders()does not exist on the webFormData, andfs.createReadStreamcannot be appended to one. Working shape:fs.openAsBlob(), and letfetchset its own boundary.
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 mTLS — cert / 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
}wtypo at line 267 (HEAD is clean) causing aReferenceErrorin thenon-realtime-sdkbranch ofsetIdentificationComplete.
Related
- FKITDEV-8947 — the scoping work that surfaced this; UniCredit
request-promise-native→ fetch migration (mTLS). - FKITDEV-9197 — CIB; the first round to implement and prove the pattern (Infocert mTLS,
undici ^6.28.0, 11/11 harness cases). - devel-dependency-removal-breaks-partner-customization — why the port was forced: devel retired
request/request-promise-nativeout from under the partner branch. - vuer_oss — the service whose
getHttpsAgent()pattern is the silent no-op.