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:
concatLoggingArgs performs unguarded primitive coercion, so it throws on
null-prototype / non-coercible values (and is lossy — [object Object] — even
when it doesn't throw).
logDebug/logInfo build 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()
returns 3 / WARN). A level guard would have skipped this code entirely.
To Reproduce
Steps to reproduce the behavior:
- Install
deepl-node@1.27.0 on 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
sendAxiosRequest reaches logDebug('Unrecognized axios error', axiosError).
- See
TypeError: Cannot convert object to primitive value thrown 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 a TypeError.
- 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-node server-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:225 is logDebug('Unrecognized axios error', axiosError);.
- Confirmed against
deepl-node@1.27.0 on Node v22.22.2: passing a top-level
null-prototype object (or an object whose Symbol.toPrimitive returns a
non-primitive) to logDebug throws; 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 — which axiosError supplies.
- Because the formatter runs regardless of log level, this affects users who have
not enabled debug logging at all.
Describe the bug
concatLoggingArgs()insrc/utils.tsformats every logged argument value with atemplate literal (
`, ${key} = ${value}`). When a top-level value cannot beconverted to a primitive — most commonly a null-prototype object (header bags
from undici/axios are created with
Object.create(null)), or an object with apathological
Symbol.toPrimitive/valueOf— the coercion throwsTypeError: Cannot convert object to primitive value.This is reached on the error path of
HttpClient.sendAxiosRequest(), which callslogDebug('Unrecognized axios error', axiosError)with the entire Axios errorobject. As a result, whenever a non-retryable transport error occurs, the library
throws a confusing
TypeErrorfrom inside its own logging code instead ofsurfacing the real
ConnectionError— the actual failure is completely masked.Two underlying defects:
concatLoggingArgsperforms unguarded primitive coercion, so it throws onnull-prototype / non-coercible values (and is lossy —
[object Object]— evenwhen it doesn't throw).
logDebug/logInfobuild the detail string eagerly, before loglevelchecks the active level. So the work (and the crash) happens even when debug
logging is disabled, which is the default (
getLogger('deepl').getLevel()returns
3/ WARN). A level guard would have skipped this code entirely.To Reproduce
Steps to reproduce the behavior:
deepl-node@1.27.0on Node.js 22.that fails at the Axios layer behind a proxy / blocked egress), so
sendAxiosRequestreacheslogDebug('Unrecognized axios error', axiosError).TypeError: Cannot convert object to primitive valuethrown fromconcatLoggingArgs, masking the original error.Minimal, self-contained reproduction of the faulty formatter (no network needed):
Observed stack trace from the real failure:
Expected behavior
to them. A non-coercible value (null-prototype object, cyclic object, etc.)
should be formatted safely, not crash the request.
ConnectionError) should be surfaced to thecaller; logging must not replace it with a
TypeError.Suggested fix (either alone prevents the crash; both are worth doing):
Guard formatting by level:
Make coercion safe, e.g. with
util.inspect: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):
deepl-nodeserver-side SDK, not a browser appSmartphone (please complete the following information):
Additional context
dist/client.js:225islogDebug('Unrecognized axios error', axiosError);.deepl-node@1.27.0on Node v22.22.2: passing a top-levelnull-prototype object (or an object whose
Symbol.toPrimitivereturns anon-primitive) to
logDebugthrows; a nested object does not (it onlystringifies to
[object Object]), so the trigger is a non-coercible value atthe top level of the args object — which
axiosErrorsupplies.not enabled debug logging at all.