Error statuses follow one rule: APIException takes a status and derives the code from it (BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403, NOT_FOUND 404, CONFLICT 409, RATE_LIMITED 429, SERVER_ERROR 500, BAD_GATEWAY 502), so a code can't contradict its status. That fixed real bugs: a wrong current password on change-password answered 401, on which authFetch signs the user out; editing or deleting someone else's comment or chat message answered 401 rather than 403; one invalid dump edit answered 422. A taken username is 409, like a taken email. Along the way: the database is created from schema.sql by connection.ts when the file doesn't exist, so every task works on a fresh checkout (dev and serve never ran db/init.ts, which is gone); the startup purge compared ISO dates with SQLite's datetime() as text and kept anything from the current UTC day, and now compares ISO cutoffs; the seeded admin's hash comes from lib/password.ts (split out of jwt.ts, which needs the JWT secret) rather than a copy of its format; avatar, thumbnail and playlist-cover uploads go through replaceUploadedFile, which writes beside the old file and renames over it before the record announces it (a failed avatar write lost the old avatar; a thumbnail was announced before it existed); upload.ts derives its size message from MAX_IMAGE_SIZE_BYTES and stops re-exporting the upload folders; ALLOWED_IMAGE_MIMES, which duplicated detectImageMime, is gone; the SPA fallback no longer calls next() after answering; a reply must answer a comment on the same dump (a foreign or unknown parent was a 500 or a cross-dump thread). 211 backend and 628 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
1.2 KiB
TypeScript
41 lines
1.2 KiB
TypeScript
/**
|
|
* Defines the exception that carries an HTTP error from any layer up to the
|
|
* error middleware.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { APIErrorCode } from "../../shared/api.ts";
|
|
|
|
/** Passed to an {@link APIException}; any other status is a type error. */
|
|
export type APIStatus = 400 | 401 | 403 | 404 | 409 | 429 | 500 | 502;
|
|
|
|
const CODES: Record<APIStatus, APIErrorCode> = {
|
|
400: APIErrorCode.BAD_REQUEST,
|
|
401: APIErrorCode.UNAUTHORIZED,
|
|
403: APIErrorCode.FORBIDDEN,
|
|
404: APIErrorCode.NOT_FOUND,
|
|
409: APIErrorCode.CONFLICT,
|
|
429: APIErrorCode.RATE_LIMITED,
|
|
500: APIErrorCode.SERVER_ERROR,
|
|
502: APIErrorCode.BAD_GATEWAY,
|
|
};
|
|
|
|
/**
|
|
* Thrown by a route, a service or a remote fetch to answer the request with an
|
|
* error. The error middleware sends it as an `APIFailure`, message included,
|
|
* so the message must be fit for the user. Use 401 for a caller who isn't
|
|
* signed in, 403 for one who is but may not, and 404 rather than 403 when the
|
|
* caller mustn't learn that the thing exists.
|
|
*/
|
|
export class APIException extends Error {
|
|
readonly code: APIErrorCode;
|
|
readonly status: APIStatus;
|
|
|
|
constructor(status: APIStatus, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
this.code = CODES[status];
|
|
}
|
|
}
|