Skip to content

fix(server): map non-enum error codes to translatable ErrorCode members - #1843

Merged
steilerDev merged 1 commit into
betafrom
fix/1811-non-enum-error-codes
Jul 7, 2026
Merged

fix(server): map non-enum error codes to translatable ErrorCode members#1843
steilerDev merged 1 commit into
betafrom
fix/1811-non-enum-error-codes

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Summary

  • DAV token 404 responses hand-rolled a DAV_TOKEN_NOT_FOUND code that was never in the ErrorCode enum — now reuses NOT_FOUND (the /profile endpoint is only ever reached via a raw <a href download> link, never the JS API client, so no code branches on the DAV-specific string).
  • The Fastify error handler passed through raw internal codes (FST_ERR_CTP_BODY_TOO_LARGE, FST_REQ_FILE_TOO_LARGE, etc.) or a made-up REQUEST_ERROR fallback, neither translatable by the client. Added a FASTIFY_ERROR_CODE_MAP for known codes plus a status-range fallback (VALIDATION_ERROR for <500, INTERNAL_ERROR for >=500). HTTP status codes are unchanged — only code changes.
  • Found PAYLOAD_TOO_LARGE was already live in production (photos.ts) with zero client translations — same bug class, fixed alongside since the new mapping routes more traffic through it.

Fixes #1811

Test plan

  • Unit tests pass (95%+ coverage) — 30/30 across davTokens.test.ts + errorHandler.test.ts, errorTranslation.test.ts 22/22
  • Integration tests pass — realistic app.inject() scenarios for oversized body, malformed/empty JSON, plus synthetic-error scenarios for multipart and fallback paths
  • Pre-commit hook quality gates pass

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com

DAV token 404 responses hand-rolled a DAV_TOKEN_NOT_FOUND code that
was never in the ErrorCode enum, so the client couldn't translate it.
Reuse NOT_FOUND instead — the client never branched on the DAV-specific
code (the /profile endpoint is only ever hit via a raw <a href download>
link, never through the JS API client).

The Fastify error handler also passed through raw Fastify/plugin
internal codes (FST_ERR_CTP_BODY_TOO_LARGE, FST_REQ_FILE_TOO_LARGE,
etc.) verbatim, or fell back to a made-up REQUEST_ERROR code — neither
mappable by translateApiError(). Added a FASTIFY_ERROR_CODE_MAP for
known codes, with a status-range fallback (VALIDATION_ERROR for <500,
INTERNAL_ERROR for >=500) for anything unmapped. HTTP status codes are
preserved exactly; only the `code` field changes.

While researching the fix, found PAYLOAD_TOO_LARGE was already a live
enum member thrown in production (photos.ts) with zero translations in
either locale — the same class of bug. Added English and German keys
since the new FST_* mapping routes more traffic through that code.

Fixes #1811

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude translator (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
@steilerDev

Copy link
Copy Markdown
Owner Author

[security-engineer] Security review of PR #1843 (fix/1811-non-enum-error-codesbeta).

Scope reviewed: server/src/plugins/errorHandler.ts (+tests), server/src/routes/davTokens.ts (+tests), client/src/i18n/{en,de}/errors.json.

Verdict: APPROVED

Analysis

A05 Security Misconfiguration / A09 Logging — error-code normalization (net positive)

  • FASTIFY_ERROR_CODE_MAP replaces raw FST_* codes and the ad-hoc REQUEST_ERROR fallback with a small, explicit map to enum ErrorCode members, plus a status-range fallback (VALIDATION_ERROR for <500, INTERNAL_ERROR for >=500) when a code is unmapped or absent. This reduces the surface of internal Fastify/plugin implementation details (FST_ERR_CTP_BODY_TOO_LARGE, FST_REQ_FILE_TOO_LARGE, etc.) leaking through the code field to the client — a genuine improvement, not just cosmetic.
  • Verified mapFastifyErrorCode() is fully typed against ErrorCode (server/src/plugins/errorHandler.ts:27-32), so it can't emit an arbitrary string — good, closes off a class of "forgot to add to the enum" bugs like the one this PR fixes.

error.message passthrough — checked, pre-existing, unaffected by this PR

  • I specifically checked whether message: error.message (server/src/plugins/errorHandler.ts:81, in the FST_*/statusCode branch) could leak internal details (paths, versions, stack info). This line is unchanged by the diff — only the code field on the same response object was touched. Confirmed via git diff origin/beta..HEAD: the message: error.message line is unmodified context, not part of the PR.
  • This branch only fires for Fastify errors that already carry an explicit statusCode (i.e., Fastify's own well-formed client/protocol errors — body-too-large, malformed JSON, bad URL, etc.), which is a distinct code path from the "Unknown/unexpected errors" branch (errorHandler.ts:86-96) that already redacts error.message in production (isProduction ? 'An internal error occurred' : error.message). Fastify's own messages for these known error classes are static/generic (e.g., "Request body is too large") or echo back only the client's own malformed input (e.g., invalid JSON snippet, bad URL) — not server internals, secrets, file paths, or version strings. No new leakage introduced, and none found pre-existing in the codes this PR maps. Not a blocking finding, but worth a follow-up hardening note below.

DAV token endpoints — auth posture unchanged

  • All four routes in davTokens.ts (GET/POST/DELETE /token, GET /profile) still start with if (!request.user) throw new UnauthorizedError() — unchanged by this PR. The /profile 404 path now throws NotFoundError('No DAV token configured') instead of hand-rolling a 404 response; status code (404) and message text are identical to before. No new unauthenticated surface, no IDOR — token status/regeneration/profile all keyed off request.user.id from the session, not from client-supplied input.
  • Both "no token" branches (!status.hasToken and !user?.davToken) now converge on the same generic NotFoundError('No DAV token configured'), which is fine — this was already true before the PR (identical message in both branches), so no new user-enumeration signal.

Locale files

  • PAYLOAD_TOO_LARGE added to both en and de (the only two supported locales) with generic, non-revealing user-facing copy. No new client-side attack surface — static JSON string additions only.

Tests

  • New errorHandler.test.ts cases cover the mapped codes, the unmapped-code fallback in both status ranges, and the missing-code case explicitly asserting the old REQUEST_ERROR/undefined behavior is gone — good regression coverage for the exact bug class this PR fixes.

Non-blocking observations (informational, no action required for merge)

  1. Informational: Consider a defense-in-depth follow-up to explicitly redact error.message for FST_* errors in production the same way the unknown-error branch does, rather than relying on Fastify's messages being safe by convention — future Fastify/plugin versions could add a code whose message is less sanitized. Not required for this PR; the current set of mapped/fallback codes is safe as verified.
  2. Informational: FASTIFY_ERROR_CODE_MAP is a static allowlist that will need occasional maintenance as new Fastify plugins are added (e.g., future multipart/rate-limit codes) — the status-range fallback already covers this gracefully in the meantime.

No injection, auth bypass, IDOR, or sensitive-data-exposure issues found. Approved.

@steilerDev steilerDev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect] APPROVED

Reviewed against the API contract (error envelope) and the ErrorCode enum. This is a clean conformance fix — it brings previously non-conformant error paths back into line with the documented envelope + enum, and every response code now resolves to a translatable enum member.

Verified

  • Error envelope unchanged. Still { error: { code, message, details? } }. HTTP status codes preserved on every path (413 stays 413, etc.). Only the code value changes.
  • All emitted codes are enum members. The three fallback/mapping targets (PAYLOAD_TOO_LARGE, VALIDATION_ERROR, INTERNAL_ERROR) all exist in shared/src/types/errors.ts, and both en and de errors.json carry translations for all of them (plus NOT_FOUND). No untranslatable code can escape anymore.
  • NOT_FOUND reuse for DAV — agree. DAV_TOKEN_NOT_FOUND was never in the enum, so it was a latent untranslatable-code bug, not a real contract surface. The /profile endpoint is reached only via a raw download link, never the JS API client, so nothing branches on the DAV-specific string. Minting a new enum member would add translation work and API surface for zero consumer benefit. Reusing NotFoundError (NOT_FOUND/404) while preserving the specific "No DAV token configured" message is the right call and consistent with the "structured codes must be enum members" principle. No lingering DAV_TOKEN_NOT_FOUND references remain in server/client/shared/e2e.
  • FST_ table + fallback.* The 13-entry table covers the realistic body/content-type/multipart failure codes. Crucially, completeness is not load-bearing: the status-range fallback guarantees any unmapped FST_* code still resolves to a valid enum member, so there is no path back to a raw FST_* string or the old made-up REQUEST_ERROR.
  • Scope containment confirmed. Only the Fastify-internal branch changed. The AppError branch (uses error.code, already enum), the AJV error.validation branch (VALIDATION_ERROR), the unknown-error branch (INTERNAL_ERROR), and the separate ROUTE_NOT_FOUND not-found handler are all untouched. The new branch keeps its !error.validation guard, and the validation branch runs first, so there is no overlap or regression.
  • Tests. New coverage exercises the real oversized-body/invalid-JSON/empty-body paths via app.inject() plus synthetic multipart and both fallback branches (<500 and >=500), and explicitly asserts the code is neither undefined nor REQUEST_ERROR. DAV test updated to assert NOT_FOUND + message.

Non-blocking notes (informational)

  • Status-range fallback semantics. A hypothetical unmapped Fastify-internal 401/403/429 would collapse to VALIDATION_ERROR. In practice those paths are all served by AppError subclasses (UNAUTHORIZED, FORBIDDEN, RATE_LIMIT_EXCEEDED), so the fallback only ever sees generic client/server errors. Acceptable at this scale — the fallback's job is to guarantee a translatable code, not perfect semantic fidelity. Worth revisiting only if a plugin ever emits an internal auth/rate-limit error outside the AppError path.
  • Doc-comment typo ("see fallbackErrorCode()" should read mapFastifyErrorCode()) — already known and deferred, not blocking.

No API-Contract or Schema wiki update required: this changes no contract surface, it restores conformance to the existing envelope and enum.

@steilerDev
steilerDev merged commit e90d68b into beta Jul 7, 2026
29 of 30 checks passed
@steilerDev
steilerDev deleted the fix/1811-non-enum-error-codes branch July 7, 2026 14:56
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.13.0-beta.11 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.13.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant