mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
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>
57 lines
2.2 KiB
TypeScript
57 lines
2.2 KiB
TypeScript
import type { Context } from 'hono'
|
|
import { createMiddleware } from 'hono/factory'
|
|
import type { Env } from './platform'
|
|
|
|
// The request boundary for /api and /dav: one structured line per request, logged
|
|
// after the response is finalized. By the time `next()` returns, the status and
|
|
// `errorLog` are settled — every error is an `AppError` (or domain error) thrown by
|
|
// the handler and rendered by `app.onError` via `jsonError`, which sets `errorLog`
|
|
// before control returns here. So the log records the REAL mapped status (a thrown
|
|
// 409 logs as 409, not 500) and carries the error's reason + full message for every
|
|
// 4xx/5xx, not just unhandled crashes.
|
|
export const accessLog = createMiddleware<Env>(async (c, next) => {
|
|
const start = Date.now()
|
|
await next()
|
|
writeAccessLog(c, start)
|
|
})
|
|
|
|
function writeAccessLog(c: Context<Env>, start: number) {
|
|
const fields = accessLogFields(c, start)
|
|
console.log(fields.map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(' '))
|
|
}
|
|
|
|
function accessLogFields(c: Context<Env>, start: number): Array<[string, string | number]> {
|
|
const status = c.res.status
|
|
const fields: Array<[string, string | number]> = [
|
|
['method', c.req.method],
|
|
['path', c.req.path],
|
|
['status', status],
|
|
['ms', Date.now() - start],
|
|
['uid', c.get('userId') ?? '-'],
|
|
]
|
|
|
|
if (c.req.path.startsWith('/dav/')) {
|
|
fields.push(
|
|
['range', c.req.header('Range') ?? '-'],
|
|
['ifRange', c.req.header('If-Range') ?? '-'],
|
|
['ifNoneMatch', c.req.header('If-None-Match') ?? '-'],
|
|
['ifModifiedSince', c.req.header('If-Modified-Since') ?? '-'],
|
|
['contentLength', c.req.header('Content-Length') ?? '-'],
|
|
['contentRange', c.res.headers.get('Content-Range') ?? '-'],
|
|
['userAgent', c.req.header('User-Agent') ?? '-'],
|
|
)
|
|
}
|
|
|
|
// Every failed request carries its reason + full message — set by jsonError when
|
|
// it renders the thrown error (incl. the full cause chain for unhandled 500s,
|
|
// which never reaches the client body).
|
|
const errorLog = c.get('errorLog')
|
|
if (errorLog) {
|
|
fields.push(['reason', errorLog.reason], ['error', errorLog.message])
|
|
} else if (status >= 400) {
|
|
fields.push(['error', c.res.statusText || '-'])
|
|
}
|
|
|
|
return fields
|
|
}
|