i18n: interpolate leaves "{{name}}" in the message when the context value is falsy (0, "", false), e.g. "Invalid parameter: {{detail}}" in error responses
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- bun, typescript
- Domain
- backend, internationalization
Research direction
The bug is in the interpolation step at src/i18n/index.ts line 84, where the truthiness test on vars[key] treats 0, empty string and false as missing. Read resolveMessage and the surrounding interpolation code, then add the regression cases from the issue to src/i18n/index.test.ts and run it with bun test. Done when falsy values are stringified and only absent or null keys keep the {{key}} placeholder.
Written by the indexing model from the issue text.
Description
Steps to reproduce
At the API level: any route that throws new DomainError("M_INVALID_PARAM", "M_INVALID_PARAM", { detail: "" }) (or { detail: 0 }) answers:
400 {"errcode":"M_INVALID_PARAM","error":"Invalid parameter: {{detail}}"}
The same happens with translate("log.net.status", { status: 0 }), which gives "status {{status}}" in the logs.
Fuzzer input (scenario 35, binary, hex), saved as inputs/i18n-interpolate-keeps-placeholder-for-falsy-value.hex:
e1e1bfe1e1e100005bffff27e1001800ffffffffffff5bffff27e1000600ffff
ffffffffffffffff27ffffffffffffff fffffffff700ffffffff27ffffffffff
ff00ffffffffff00ff4aff617a5f7a6572
The harness decodes it to several requests. The failing one hits a route throwing DomainError("M_INVALID_PARAM", {detail: ""}), and the answer is "Invalid parameter: {{detail}}".
Regression test (bun:test, for example src/i18n/index.test.ts):
import { beforeAll, describe, expect, it } from "bun:test";
import { loadMessages, resolveMessage } from "./index";
describe("resolveMessage interpolation", () => {
beforeAll(() => {
loadMessages("assets/i18n", { debug() {}, info() {}, warn() {}, error() {} } as any);
});
it.each([["", "Invalid parameter: "], [0, "Invalid parameter: 0"], [false, "Invalid parameter: false"]])(
"fills {{detail}} with %p",
(detail, expected) => {
expect(resolveMessage("en", "M_INVALID_PARAM", { detail })).toBe(expected);
},
);
it("keeps the placeholder only when the key is absent", () => {
expect(resolveMessage("en", "M_INVALID_PARAM", {})).toBe("Invalid parameter: {{detail}}");
});
});
Expected
A placeholder whose key the context supplies is replaced by String(value), whatever the value (0 gives "0", "" gives an empty string, false gives "false"). Only a key that is absent (or undefined/null) may keep {{key}}.
Observed
Scenario 35 oracle (reproduced):
Error: a placeholder whose value the context gives was left in "Invalid parameter: {{detail}}" (context {"detail":""})
Possible cause
src/i18n/index.ts:84:
return vars[key] ? String(vars[key]) : `{{${key}}}`;
The truthiness test treats 0, "" and false as missing. Suggested fix: test for presence instead, e.g. Object.hasOwn(vars, key) && vars[key] != null ? String(vars[key]) : \{{${key}}}`. Object.hasOwnalso stops keys such as{{constructor}}` from resolving to inherited properties of the context object.
Introduced by
Not found: interpolate comes from 4d406b7 (2026-09-15, "chore: initial import"), which squashes the earlier history. No later commit changed line 84.
Impact
The context values come from the server's own code. A client can reach this only when a route copies client-controlled data that can be empty or zero into a DomainError context (for example an empty parameter name or value reported in detail). The visible effect is an error message with a literal {{detail}}, or a log line such as status {{status}} that hides a real value (a status of 0 for a network failure is exactly what an operator needs to see). Status codes and errcode are unaffected. There is no availability, integrity or confidentiality impact.
Fuzzing scenario
35-cors-and-error-responses (CORS decision, errorMiddleware / i18n error messages and requestId behind the middleware chain of createApp). Found during the first run on commit e2e64af072bd45835ff6738234014135b4f3793d.
Found by twake-fuzz (coverage-guided fuzzing), scenario 35-cors-and-error-responses, commit e2e64af.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 3
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 19
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from linagora/ToM-Server
-
bug claude Minor static-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#126 ·
Maintainers usually reply within 1 day
-
bug claude Minor static-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
linagora/ToM-Server#125 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#123 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
linagora/ToM-Server#118 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
linagora/ToM-Server#117 ·
Maintainers usually reply within 1 day
All issues in linagora/ToM-Server
Similar issues
-
Add "append" and "prepend" slots for vue-vuetify extended renderersPossibly taken @simeyc claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
eclipsesource/jsonforms#2668 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
README の通知条件を実際の判定処理に合わせるPossibly taken @unhappychoice claimed this today. Openfactory:in-progress priority:medium
Difficulty 1/5 Under an hour Newbie friendliness 90/100
unhappychoice/oss-issue-opener#395 · 1 comment ·
Maintainers usually reply within 1 day
-
bug Website
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
OWASP/OWASP-Hunting#88 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day