fix(api): every error body is a coded message with the right code, args and owner #279

Merged
dominik.polakovics merged 3 commits from afk/272 into main 2026-10-13 23:46:47 +02:00

Closes #272

What changes on the wire

  • Page-path rule, one owner. pageops.ValidatePagePath(param, page, langs) is the only place that checks a page path (empty, not local, translation file). The server calls it, and its own copy is gone.

    • An empty path answers 400 request.missingParam {name}, where name is the key the route reads (page, path, fromPage or toPage).
    • A translation file's path answers 400 page.translationPath on every route.
    • page.pathMissing and editor.translationPath are removed from the registry and the catalogs.
  • Engine refusals have their own codes. content.MutateError gains a Kind. One mapping in apimsg.FromError (internal/apimsg/engine.go) turns each engine error into a code:

    • element.outOfRange, element.notContainer, element.moveIntoItself, element.rootPath and element.invalidNode: 409, as before.
    • page.parseFailed and page.serializeRefused: 422.

    Every template is {detail}, so the English text and the statuses are unchanged.

    • Deviation from the brief: the issue put this mapping in writeError/writeOpError. I put it in FromError because the server's writeError and pageops' passThrough both already call it, and pageops can't import the server. So pageops' create serializer refusals get the code without a second copy of the mapping. apimsg importing content creates no cycle, and content still doesn't import apimsg.
  • Escaping media ref → 400 library.invalidRef {ref}. The rule is coded once, in media.ResolvePath. Media info, delete, rename and move all answer it, mediaops included. The text no longer contains the absolute media root.

  • media.folderNotFound. args.folder is now the folder as sent (outer slashes trimmed). The library root can be missing (no media directory), and that case gets its own code: 404 media.rootNotFound, with no args.

  • Smaller fixes:

    • translation.appeared {path} is content-root-relative.
    • A failed publish job always carries args, {} when the message names nothing.
    • field.unknown echoes items[0].foo as sent.
    • A wrong method on GET /api/status answers a JSON 405 request.method.
    • The five stale doc comments are fixed.
  • docs/api.md is updated for each item. error.unexpected is now listed only for underlying failures (filesystem, git, front-matter apply, re-encoding).

Not affected

scaffold/site/AGENTS.md, the scaffolded skills, docs/harness.md and docs/architecture.md. None of them names an affected code (checked with grep).

Verification

  • go build ./..., go vet ./..., gofmt -l and go test ./... all pass, including TestCallSites, TestExport, the route test and docs/docs_test.go. cd web && npm test passes 3210 tests.
  • New tests cover each reproduction row:
    • TestPagePathRefusals: missing page/path and translation paths.
    • TestEngineRefusalCodes: a stale [5] delete gives 409 element.outOfRange with the old text as its detail.
    • TestCodedErrorBodies: POST /api/status gives a JSON 405.
    • Also: TestValidatePagePath, TestMutateErrorKind, TestFromErrorEngine, TestResolvePathRefusalsAreCoded, TestCodedEscapingMediaRef, TestCodedMediaFolderNotFound, TestTranslateAppearedNamesRelativePath and TestFormCheckUnknownField, plus the publish job's "args":{}.
  • grep -rn "editor.translationPath\|page.pathMissing" internal web/src docs finds nothing.

🤖 Generated with Claude Code

Closes #272 ## What changes on the wire - **Page-path rule, one owner.** `pageops.ValidatePagePath(param, page, langs)` is the only place that checks a page path (empty, not local, translation file). The server calls it, and its own copy is gone. - An empty path answers `400 request.missingParam {name}`, where `name` is the key the route reads (`page`, `path`, `fromPage` or `toPage`). - A translation file's path answers `400 page.translationPath` on every route. - `page.pathMissing` and `editor.translationPath` are removed from the registry and the catalogs. - **Engine refusals have their own codes.** `content.MutateError` gains a `Kind`. One mapping in `apimsg.FromError` (`internal/apimsg/engine.go`) turns each engine error into a code: - `element.outOfRange`, `element.notContainer`, `element.moveIntoItself`, `element.rootPath` and `element.invalidNode`: 409, as before. - `page.parseFailed` and `page.serializeRefused`: 422. Every template is `{detail}`, so the English text and the statuses are unchanged. - **Deviation from the brief:** the issue put this mapping in `writeError`/`writeOpError`. I put it in `FromError` because the server's `writeError` and pageops' `passThrough` both already call it, and pageops can't import the server. So pageops' create serializer refusals get the code without a second copy of the mapping. apimsg importing content creates no cycle, and content still doesn't import apimsg. - **Escaping media ref → `400 library.invalidRef {ref}`.** The rule is coded once, in `media.ResolvePath`. Media info, delete, rename and move all answer it, mediaops included. The text no longer contains the absolute media root. - **`media.folderNotFound`.** `args.folder` is now the folder as sent (outer slashes trimmed). The library root can be missing (no media directory), and that case gets its own code: `404 media.rootNotFound`, with no args. - **Smaller fixes:** - `translation.appeared {path}` is content-root-relative. - A failed publish job always carries `args`, `{}` when the message names nothing. - `field.unknown` echoes `items[0].foo` as sent. - A wrong method on `GET /api/status` answers a JSON `405 request.method`. - The five stale doc comments are fixed. - **`docs/api.md`** is updated for each item. `error.unexpected` is now listed only for underlying failures (filesystem, git, front-matter apply, re-encoding). ## Not affected `scaffold/site/AGENTS.md`, the scaffolded skills, `docs/harness.md` and `docs/architecture.md`. None of them names an affected code (checked with grep). ## Verification - `go build ./...`, `go vet ./...`, `gofmt -l` and `go test ./...` all pass, including `TestCallSites`, `TestExport`, the route test and `docs/docs_test.go`. `cd web && npm test` passes 3210 tests. - New tests cover each reproduction row: - `TestPagePathRefusals`: missing `page`/`path` and translation paths. - `TestEngineRefusalCodes`: a stale `[5]` delete gives 409 `element.outOfRange` with the old text as its detail. - `TestCodedErrorBodies`: POST `/api/status` gives a JSON 405. - Also: `TestValidatePagePath`, `TestMutateErrorKind`, `TestFromErrorEngine`, `TestResolvePathRefusalsAreCoded`, `TestCodedEscapingMediaRef`, `TestCodedMediaFolderNotFound`, `TestTranslateAppearedNamesRelativePath` and `TestFormCheckUnknownField`, plus the publish job's `"args":{}`. - `grep -rn "editor.translationPath\|page.pathMissing" internal web/src docs` finds nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The server re-implemented pageops' page-path rule with different codes.
pageops.ValidatePagePath is now the only evaluation; the server calls it
with the request key the route reads, so an empty path answers
request.missingParam naming page or path, and a translation file's path
answers page.translationPath everywhere. page.pathMissing and
editor.translationPath are retired.

Refs #272

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
content.MutateError gains a Kind, and apimsg.FromError maps each kind,
a content.ParseError and a content.SerializeError to a code
(element.outOfRange, element.notContainer, element.moveIntoItself,
element.rootPath, element.invalidNode, page.parseFailed,
page.serializeRefused) instead of error.unexpected. Statuses and English
text are unchanged; content still does not import apimsg.

Refs #272

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(api): code the remaining uncoded and mis-argued error bodies
All checks were successful
ci / build-and-test (pull_request) Successful in 3m9s
ci / release (pull_request) Has been skipped
c9a7ee66b3
- an escaping media ref is library.invalidRef {ref}, decided once in
  media.ResolvePath, from every media route and mediaops
- media.folderNotFound carries the folder as sent; a missing library
  root answers media.rootNotFound instead of English inside args
- translation.appeared names the content-root-relative path
- a failed publish job always carries args, {} when empty
- field.unknown echoes an item-row key as sent (items[0].foo)
- a wrong method on GET /api/status answers a JSON request.method
- five stale doc comments say what the code does

Refs #272

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS — validated against the landing core.

Signal: labctl pr checks 279 aggregate success (ci / build-and-test: npm ci/build/test, go vet, go test ./..., go build; release skipped as expected on a PR). Not re-run locally. gofmt -l clean. Branch is current with main (no commits on main missing from afk/272).

Conventions: Conventional Commits title; Closes #272 present; diff scope matches the issue's acceptance criteria item by item:

  • page-path rule has one owner, pageops.ValidatePagePath (internal/pageops/paths.go:536); server's copy removed (internal/server/server.go:704-765), each route passes its own key (page/path/fromPage/toPage).
  • engine refusals coded in one place, apimsg.engineMessage (internal/apimsg/engine.go), reached via FromError. This deviates from the brief's writeError, and the PR explains why: a single mapping that pageops passThrough reaches too. content still does not import apimsg.
  • escaping media ref → library.invalidRef in media.ResolvePath (internal/media/library.go:413,422); mediaops passes it on.
  • media.folderNotFound args are the folder as sent; root case has media.rootNotFound (internal/mediaops/paths.go:446).
  • translation.appeared relative path, publish job args always {}, field.unknown echoes items[i].x, /api/status JSON 405, doc comments fixed.
  • docs/api.md updated per route; retired codes absent from internal, web/src, docs and scaffold (grep). en/de catalogs updated. Scaffold AGENTS.md and skills are untouched, and no code they name was affected.
  • No web/src code matches on any changed English text.
[autoland] verdict: pass PASS — validated against the landing core. Signal: `labctl pr checks 279` aggregate **success** (ci / build-and-test: npm ci/build/test, go vet, go test ./..., go build; release skipped as expected on a PR). Not re-run locally. gofmt -l clean. Branch is current with main (no commits on main missing from afk/272). Conventions: Conventional Commits title; `Closes #272` present; diff scope matches the issue's acceptance criteria item by item: - page-path rule has one owner, `pageops.ValidatePagePath` (internal/pageops/paths.go:536); server's copy removed (internal/server/server.go:704-765), each route passes its own key (page/path/fromPage/toPage). - engine refusals coded in one place, `apimsg.engineMessage` (internal/apimsg/engine.go), reached via FromError. This deviates from the brief's writeError, and the PR explains why: a single mapping that pageops passThrough reaches too. content still does not import apimsg. - escaping media ref → library.invalidRef in media.ResolvePath (internal/media/library.go:413,422); mediaops passes it on. - media.folderNotFound args are the folder as sent; root case has media.rootNotFound (internal/mediaops/paths.go:446). - translation.appeared relative path, publish job args always `{}`, field.unknown echoes items[i].x, /api/status JSON 405, doc comments fixed. - docs/api.md updated per route; retired codes absent from internal, web/src, docs and scaffold (grep). en/de catalogs updated. Scaffold AGENTS.md and skills are untouched, and no code they name was affected. - No web/src code matches on any changed English text.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!279
No description provided.