Collapse the two error conventions (string-reason `{ok:false,reason}` outcomes
and thrown domain-error classes) onto one. Usecases now produce typed `AppError`
values via factories (`notFound()`/`quotaExceeded()`/`featureBlocked()`/…);
handlers `throw result.error`; and `jsonError` (renamed from `renderError`) is the
single place that renders any error to an AIP-193 body + access-log line, in
`app.onError`/accessLog.
Why: the previous setup had a string→code mapping (`outcomeError` + the `OUTCOME`
table) living in parallel with a type→code mapping (`mapDomainError`), plus inline
`apiError(c, <status>, …)` calls that hand-wrote the status at every site — exactly
the drift that left the same `quota_exceeded` at 400 in one handler and 422 in the
rest. Now the status/reason live once, in the factory.
- Add `server/usecases/ports/app-error.ts`: `AppError` + factories. Status/reason
are baked in per factory, so no usecase or handler writes an HTTP code or a
magic-string reason. `AppError` also carries optional response headers
(`Retry-After`) via a `rateLimited()` factory.
- Delete `apiError`, `outcomeError`, the `OUTCOME` table, and the dead `ApiError`
class. The 67 inline guard/middleware `apiError` sites became `throw <factory>()`.
- Control-flow outcomes a handler branches on (not just renders) stay discriminated
reasons (e.g. `deleteObject` `not_trashed`); internal shared sub-usecases
(traffic-metering, licensing internals) keep string reasons, mapped at the boundary.
- Regenerate the Go OpenAPI client (saveShare gained a 422 response).
BREAKING CHANGE: POST /shares/{token}/objects quota rejection now returns 422
(was an inconsistent 400); every other quota path already returned 422.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(api)!: unify errors to AIP-193 + Page<T> pagination, enrich access log (#443)
Settle the API consistency issues from #443 before SDKs ship. Breaking changes
across the error envelope, list envelopes, and the generated Go client.
Errors → AIP-193 google.rpc.Status (https://google.aip.dev/193):
- every error body is now { error: { code, message, status, details:[ErrorInfo] } }
- machine-readable, switchable key is details[0].reason (UPPER_SNAKE); status is the
canonical google.rpc.Code; dynamic context lives in metadata (string→string)
- built once in server/lib/http-errors.ts (buildErrorBody/ApiError/mapDomainError);
inline handlers use apiError(c,status,msg,opts?); thrown errors flow through
app.onError → renderError. Resolves#8 (one casing; no-storage 503 everywhere) and
#9 (resource/maxBytes/conflictingName/licensing fields folded into metadata;
featureGateErrorSchema removed)
Pagination → Page<T> = { items, total, page, pageSize } via pageSchema + integer
pageQuerySchema, applied to every list endpoint. image-hosting/images stays cursor
(the one intentional exception). unreadCount moved out of the notifications list into
/notifications/stats; entitlements drop the redundant orgId; team invitations use items.
Access log: every 4xx/5xx carries reason + full message (set by apiError and
renderError); a thrown domain error logs its mapped status (409, not 500); unhandled
500s log the full cause chain while the client gets a generic message.
Frontend ApiError exposes reason/metadata/canonicalStatus; consumers updated. Go
client regenerated from the new OpenAPI document.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(api): fix e2e name-conflict assertion + cover AIP-193 error branches
- e2e/name-conflict.spec.ts: assert body.error.details[0].reason (AIP-193) instead
of the removed top-level body.code
- unit-test buildErrorBody, ApiError, and every mapDomainError branch
(server/lib/http-errors.test.ts) and renderError + isHandledError
(server/middleware/error-handler.test.ts)
- integration-test the apiError error-branch guards the refactor touched:
shares, redirect, site/invitations, objects, store/storefront, and the
requirePermission middleware (authz) — restoring patch coverage above target
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(api): drop ad-hoc [spec:] breadcrumbs from new coverage tests
lint:spec governs spec↔test traceability: a [spec: id] breadcrumb must map to a
documented @id scenario in spec/**/*.feature. The added error-branch coverage
tests are not Gherkin scenarios, so reference no spec id — use plain titles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(objects): allow the file-manager pageSize (500) on the objects list
The shared pageQuerySchema caps pageSize at 100, but the file manager loads a
whole folder client-side (FILES_PAGE_SIZE=500, transfer dialog 200) — the old
z.string() query param was unbounded. With the cap, GET /api/objects?pageSize=500
returned 400, the file-manager list query errored and retried, and the toolbar /
table never rendered (e2e: responsive @desktop + name-conflict table state). Raise
just this list's ceiling to 1000 (default stays 20); other lists keep the 100 cap.
Regression-tested: GET /api/objects?pageSize=500 → 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>