Skip to content

Two error-code vocabularies are both live: StandardErrorCode is lowercase snake_case, the servers emit SCREAMING_SNAKE #3841

Description

@os-zhuang

Noted as "Related, not the same" in #3689 and carried out of it when #3837 landed, so it is filed here rather than left buried in a closed issue. This one is a spec decision, not a bug fix, and it should be settled before #3689's other sibling (error.code occupied by the HTTP status) is touched — otherwise that fix has no vocabulary to migrate to and would have to be redone.

The drift

packages/spec/src/api/errors.zod.ts declares StandardErrorCode as a closed enum of lowercase snake_case codes:

export const StandardErrorCode = z.enum([
  'validation_error', 'invalid_field', 'missing_required_field', 
  'unauthenticated', 'invalid_credentials', 'expired_token', 
  'permission_denied', 'insufficient_privileges', 
]);

It is the declared type of FieldErrorSchema.code and EnhancedApiErrorSchema.code, and content/docs/api/error-catalog.mdx documents it as the catalog.

Meanwhile the wire is majority SCREAMING_SNAKE. Counting distinct literals in non-test source under packages/:

Vocabulary Distinct codes Examples
SCREAMING_SNAKE 139 AUTH_REQUIRED, FILE_NOT_FOUND, INVALID_REQUEST, PERMISSION_DENIED, ATTACHMENT_DOWNLOAD_DENIED, UPLOAD_SESSION_NOT_FOUND
lowercase snake_case ~100 validation_error, forbidden, bad_request, driver_missing, checksum_drift

(The lowercase count includes a handful of false positives — code: 'custom', code: 'finance' — but the shape of the split is not in doubt.)

The split runs through a single request path. http-dispatcher.ts:1379 parks { code: 'PERMISSION_DENIED' } — SCREAMING — in details, while the enum that names the same condition calls it permission_denied. Neither ApiErrorSchema.code nor the routes reference StandardErrorCode at all: ApiErrorSchema declares a bare z.string(), so nothing is validated and both dialects pass.

Why it matters now, not before

#3687 (for #3675) and #3837 (for #3689) moved the storage and i18n services into the declared envelope without reconciling the vocabulary — deliberately, because the envelope was mechanical and this is not. With the envelope settled on both paths, the code field is the remaining unenforced part of ApiErrorSchema, and it is the part consumers actually branch on: the console's attachment panel maps ATTACHMENT_DOWNLOAD_DENIED / AUTH_REQUIRED to user-facing copy, and the dogfood suite asserts on them.

What needs deciding

  1. Adopt SCREAMING_SNAKE — rename the enum's members, rewrite error-catalog.mdx. Matches what 139 codes already emit and what every consumer already reads, so no runtime migration. But StandardErrorCode is an exported spec type, so this is a breaking rename for anyone importing it, and the ~100 lowercase emitters still need a sweep.
  2. Adopt lowercase snake_case — keep the enum, migrate the 139 emitters. Consistent with the documented catalog and with the repo's snake_case-for-data-values convention (Prime Directive Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3), but it changes strings the console and the dogfood suite branch on, so it needs the consumers in hand.
  3. Declare them different thingsStandardErrorCode stays the field-level validation vocabulary (FieldErrorSchema), and top-level error.code gets its own declared enum in the other dialect. Honest about current usage; costs one more concept.

Recommendation: option 1, on the grounds that the wire is the harder thing to move and 139 > 100. Whichever wins, ApiErrorSchema.code should stop being z.string() and start referencing the chosen enum — otherwise this reopens the first time someone types a new code, and the conformance suites added in #3687/#3837 have nothing to assert the value against, only the shape.

Guard note

error-envelope.conformance.test.ts and success-envelope.conformance.test.ts (service-storage, service-i18n) already drive every branch and parse against the imported spec schemas. Once code is a real enum they get the value check for free — the schema tightens, the suites do not change.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions