-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherror-handler.ts
More file actions
113 lines (99 loc) · 3.97 KB
/
Copy patherror-handler.ts
File metadata and controls
113 lines (99 loc) · 3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import type { FastifyError, FastifyInstance, FastifySchemaValidationError } from 'fastify';
import { ZodError, type ZodIssue } from 'zod';
import { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';
/** The single error shape every failure renders as: `{ error: { code, message } }`. */
export interface ErrorEnvelope {
error: { code: string; message: string };
}
/**
* An application-level failure that carries its own HTTP status and stable
* error code (e.g. a `404` for an unknown Property). Services throw these; the
* central handler renders them into the envelope.
*/
export class AppError extends Error {
readonly statusCode: number;
readonly code: string;
constructor(statusCode: number, code: string, message: string) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.code = code;
}
}
function envelope(code: string, message: string): ErrorEnvelope {
return { error: { code, message } };
}
/** Render a set of (field path, message) pairs as one `; `-joined message. */
function formatIssues(issues: { path: string; message: string }[]): string {
return issues.map(({ path, message }) => (path ? `${path}: ${message}` : message)).join('; ');
}
/** Collapse raw Zod issues (thrown in application code) into one message. */
function normalizeZodIssues(issues: ZodIssue[]): string {
return formatIssues(
issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message })),
);
}
/**
* Collapse Fastify's schema-validation entries (as produced by the Zod
* type-provider) into one message. Each entry carries a JSON-pointer
* `instancePath` like `/count`, which we render as a dotted field path.
*/
function normalizeValidationErrors(errors: FastifySchemaValidationError[]): string {
return formatIssues(
errors.map((error) => ({
path: error.instancePath.replace(/^\//, '').replace(/\//g, '.'),
message: error.message ?? 'Invalid value',
})),
);
}
function defaultCodeForStatus(statusCode: number): string {
switch (statusCode) {
case 400:
return 'BAD_REQUEST';
case 404:
return 'NOT_FOUND';
default:
return 'ERROR';
}
}
/**
* Register the central error and not-found handlers. Every failure — Zod
* request-validation errors, raw ZodErrors thrown in services, explicit
* AppErrors, framework HTTP errors, and unexpected exceptions — is normalized
* into `{ error: { code, message } }` so clients handle failures uniformly.
*/
export function registerErrorHandler(app: FastifyInstance): void {
app.setNotFoundHandler((request, reply) => {
void reply
.status(404)
.send(envelope('NOT_FOUND', `Route ${request.method} ${request.url} not found`));
});
app.setErrorHandler((error: FastifyError, request, reply) => {
// Request validation failures raised by the Zod type-provider.
if (hasZodFastifySchemaValidationErrors(error)) {
void reply
.status(400)
.send(envelope('VALIDATION_ERROR', normalizeValidationErrors(error.validation)));
return;
}
// A raw ZodError thrown in application code (e.g. a service-level guard).
if (error instanceof ZodError) {
void reply.status(400).send(envelope('VALIDATION_ERROR', normalizeZodIssues(error.issues)));
return;
}
// Application errors carry their own status and stable code.
if (error instanceof AppError) {
void reply.status(error.statusCode).send(envelope(error.code, error.message));
return;
}
// Framework/HTTP errors that carry a client-error status.
const statusCode = typeof error.statusCode === 'number' ? error.statusCode : 500;
if (statusCode >= 400 && statusCode < 500) {
void reply.status(statusCode).send(envelope(defaultCodeForStatus(statusCode), error.message));
return;
}
// Anything else is unexpected: log the detail, hide it from the client.
request.log.error(error);
void reply.status(500).send(envelope('INTERNAL_ERROR', 'An unexpected error occurred'));
});
}