Files
reader/server/src/http/errors.ts
T
cnb.dQC5J6ohAGA 2233b295bb feat: 初始化自部署书库服务端
按产品设计 v0.2 落地服务端,覆盖第一版范围中的扫描、元数据、权限与同步。

核心决策:
- 书籍主键用 dc:identifier + 内容哈希,不用文件路径。
  重命名/移动不丢进度,副本自动合并,重新编码的文件保持独立。
- 库目录只读挂载,写操作全部落到 DATA_DIR。
  启动时断言 DATA_DIR 不在 BOOKS_DIR 内,路径解析统一走 resolveInside
  拦截逃逸,扫描不跟随符号链接。
- 元数据优先级:手动修改 > 内嵌 > provider > 文件名解析。
  手动修改独立成表,扫描永远碰不到,可单字段撤销。
- 中文文件名解析采取保守策略,无证据时不猜测作者方向。
- 离线合并用 LWW,笔记删除写墓碑,批次单事务。
- 无原生编译依赖:node:sqlite 与 scrypt 均为内置模块。

交付:
- Fastify + TypeScript 服务端,Docker 单容器
- 27 个自动化测试用例全部通过
- AGPL-3.0,README 做到一条命令跑起来
- CNB 流水线:typecheck / test / build
2026-09-16 14:07:25 +08:00

34 lines
1.3 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import { AppError } from '../lib/errors.ts';
/**
* Single error serialiser. Responses intentionally keep the same shape across
* every endpoint so old clients keep working as the API grows (§8.3).
*/
export function registerErrorHandler(app: FastifyInstance): void {
app.setErrorHandler((error, request, reply) => {
if (error instanceof AppError) {
reply.status(error.statusCode).send({ error: { code: error.code, message: error.message } });
return;
}
const fastifyError = error as { statusCode?: number; message?: string };
const status = fastifyError.statusCode ?? 500;
if (status >= 500) {
request.log.error({ err: error }, 'unhandled error');
reply.status(status).send({ error: { code: 'INTERNAL', message: 'internal server error' } });
return;
}
// Validation and content-type failures land here; they are the client's
// fault, so the message is passed through verbatim.
reply.status(status).send({
error: { code: 'BAD_REQUEST', message: fastifyError.message || 'bad request' },
});
});
app.setNotFoundHandler((request, reply) => {
reply.status(404).send({
error: { code: 'NOT_FOUND', message: `no route for ${request.method} ${request.url}` },
});
});
}