Repository navigation
Description
Describe the bug
concatLoggingArgs() in src/utils.ts formats every logged argument value with a
template literal (`, ${key} = ${value}`). When a top-level value cannot be
converted to a primitive — most commonly a null-prototype object (header bags
from undici/axios are created with Object.create(null)), or an object with a
pathological Symbol.toPrimitive/valueOf — the coercion throws
TypeError: Cannot convert object to primitive value.
This is reached on the error path of HttpClient.sendAxiosRequest(), which calls
logDebug('Unrecognized axios error', axiosError) with the entire Axios error
object. As a result, whenever a non-retryable transport error occurs, the library
throws a confusing TypeError from inside its own logging code instead of
surfacing the real ConnectionError — the actual failure is completely masked.
Two underlying defects:
concatLoggingArgsperforms unguarded primitive coercion, so it throws on
null-prototype / non-coercible values (and is lossy —[object Object]— even
when it doesn't throw).logDebug/logInfobuild the detail string eagerly, before loglevel
checks the active level. So the work (and the crash) happens even when debug
logging is disabled, which is the default (getLogger('deepl').getLevel()
returns3/ WARN). A level guard would have skipped this code entirely.
To Reproduce
Steps to reproduce the behavior:
- Install
deepl-node@1.27.0on Node.js 22. - Trigger any non-retryable transport error on a request (e.g. a translate call
that fails at the Axios layer behind a proxy / blocked egress), so
sendAxiosRequestreacheslogDebug('Unrecognized axios error', axiosError). - See
TypeError: Cannot convert object to primitive valuethrown from
concatLoggingArgs, masking the original error.
Minimal, self-contained reproduction of the faulty formatter (no network needed):
const { logDebug } = require('deepl-node/dist/utils');
const loglevel = require('loglevel');
console.log('deepl logger level =', loglevel.getLogger('deepl').getLevel()); // 3 (WARN) by default
// A null-prototype object as a top-level arg value — exactly what an
// Axios/undici error carries (e.g. a header bag).
const headers = Object.create(null);
headers['content-type'] = 'application/json';
logDebug('Unrecognized axios error', { headers });
// → TypeError: Cannot convert object to primitive value
// (thrown even though the level is WARN and nothing would be printed)Observed stack trace from the real failure:
TypeError: Cannot convert object to primitive value
at concatLoggingArgs (node_modules/deepl-node/dist/utils.js:17:37)
at logDebug (node_modules/deepl-node/dist/utils.js:23:28)
at HttpClient.sendAxiosRequest (node_modules/deepl-node/dist/client.js:225:38)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async HttpClient.sendRequestWithBackoff (node_modules/deepl-node/dist/client.js:162:28)
at async Translator.translateText (node_modules/deepl-node/dist/translator.js:191:41)
Expected behavior
- Logging helpers must never throw, regardless of the shape of the values passed
to them. A non-coercible value (null-prototype object, cyclic object, etc.)
should be formatted safely, not crash the request. - The original transport error (
ConnectionError) should be surfaced to the
caller; logging must not replace it with aTypeError. - With logging disabled (default WARN level), the formatter should not run at all.
Suggested fix (either alone prevents the crash; both are worth doing):
-
Guard formatting by level:
function logDebug(message, args) { if (logger.getLevel() <= loglevel.levels.DEBUG) { logger.debug(message + concatLoggingArgs(args)); } }
-
Make coercion safe, e.g. with
util.inspect:const { inspect } = require('util'); function concatLoggingArgs(args) { let detail = ''; if (args) { for (const [key, value] of Object.entries(args)) { const str = typeof value === 'object' && value !== null ? inspect(value, { depth: 2, breakLength: Infinity }) : String(value); detail += `, ${key} = ${str}`; } } return detail; }
The error branch could also log
axiosError.message/axiosError.code
(strings) rather than the whole error object, matching the retryable branch
which already uses`Encountered a retryable-error: ${error.message}`.
Screenshots
N/A (server-side library; stack trace and repro included above).
Desktop (please complete the following information):
- OS: Linux (x86_64)
- Browser: N/A — this is the
deepl-nodeserver-side SDK, not a browser app - Version: deepl-node 1.27.0; Node.js v22.22.2
Smartphone (please complete the following information):
- N/A (server-side library)
Additional context
dist/client.js:225islogDebug('Unrecognized axios error', axiosError);.- Confirmed against
deepl-node@1.27.0on Node v22.22.2: passing a top-level
null-prototype object (or an object whoseSymbol.toPrimitivereturns a
non-primitive) tologDebugthrows; a nested object does not (it only
stringifies to[object Object]), so the trigger is a non-coercible value at
the top level of the args object — whichaxiosErrorsupplies. - Because the formatter runs regardless of log level, this affects users who have
not enabled debug logging at all.
Activity