@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handling
|
|
3
|
+
description: Error handling patterns in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: errors, exceptions, error-handler, validation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Error Handling in Fastify
|
|
9
|
+
|
|
10
|
+
## Default Error Handler
|
|
11
|
+
|
|
12
|
+
Fastify has a built-in error handler. Thrown errors automatically become HTTP responses:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
|
|
17
|
+
const app = Fastify({ logger: true });
|
|
18
|
+
|
|
19
|
+
app.get('/users/:id', async (request) => {
|
|
20
|
+
const user = await findUser(request.params.id);
|
|
21
|
+
if (!user) {
|
|
22
|
+
// Throwing an error with statusCode sets the response status
|
|
23
|
+
const error = new Error('User not found');
|
|
24
|
+
error.statusCode = 404;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return user;
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Custom Error Classes
|
|
32
|
+
|
|
33
|
+
Use `@fastify/error` for creating typed errors:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import createError from '@fastify/error';
|
|
37
|
+
|
|
38
|
+
const NotFoundError = createError('NOT_FOUND', '%s not found', 404);
|
|
39
|
+
const UnauthorizedError = createError('UNAUTHORIZED', 'Authentication required', 401);
|
|
40
|
+
const ForbiddenError = createError('FORBIDDEN', 'Access denied: %s', 403);
|
|
41
|
+
const ValidationError = createError('VALIDATION_ERROR', '%s', 400);
|
|
42
|
+
const ConflictError = createError('CONFLICT', '%s already exists', 409);
|
|
43
|
+
|
|
44
|
+
// Usage
|
|
45
|
+
app.get('/users/:id', async (request) => {
|
|
46
|
+
const user = await findUser(request.params.id);
|
|
47
|
+
if (!user) {
|
|
48
|
+
throw new NotFoundError('User');
|
|
49
|
+
}
|
|
50
|
+
return user;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
app.post('/users', async (request) => {
|
|
54
|
+
const exists = await userExists(request.body.email);
|
|
55
|
+
if (exists) {
|
|
56
|
+
throw new ConflictError('Email');
|
|
57
|
+
}
|
|
58
|
+
return createUser(request.body);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Custom Error Handler
|
|
63
|
+
|
|
64
|
+
Implement a centralized error handler:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import Fastify from 'fastify';
|
|
68
|
+
import type { FastifyError, FastifyRequest, FastifyReply } from 'fastify';
|
|
69
|
+
|
|
70
|
+
const app = Fastify({ logger: true });
|
|
71
|
+
|
|
72
|
+
app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
|
|
73
|
+
// Log the error
|
|
74
|
+
request.log.error({ err: error }, 'Request error');
|
|
75
|
+
|
|
76
|
+
// Handle validation errors
|
|
77
|
+
if (error.validation) {
|
|
78
|
+
return reply.code(400).send({
|
|
79
|
+
statusCode: 400,
|
|
80
|
+
error: 'Bad Request',
|
|
81
|
+
message: 'Validation failed',
|
|
82
|
+
details: error.validation,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle known errors with status codes
|
|
87
|
+
const statusCode = error.statusCode ?? 500;
|
|
88
|
+
const code = error.code ?? 'INTERNAL_ERROR';
|
|
89
|
+
|
|
90
|
+
// Don't expose internal error details in production
|
|
91
|
+
const message = statusCode >= 500 && process.env.NODE_ENV === 'production'
|
|
92
|
+
? 'Internal Server Error'
|
|
93
|
+
: error.message;
|
|
94
|
+
|
|
95
|
+
return reply.code(statusCode).send({
|
|
96
|
+
statusCode,
|
|
97
|
+
error: code,
|
|
98
|
+
message,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Error Response Schema
|
|
104
|
+
|
|
105
|
+
Define consistent error response schemas:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
app.addSchema({
|
|
109
|
+
$id: 'httpError',
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
statusCode: { type: 'integer' },
|
|
113
|
+
error: { type: 'string' },
|
|
114
|
+
message: { type: 'string' },
|
|
115
|
+
details: {
|
|
116
|
+
type: 'array',
|
|
117
|
+
items: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
field: { type: 'string' },
|
|
121
|
+
message: { type: 'string' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ['statusCode', 'error', 'message'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Use in route schemas
|
|
130
|
+
app.get('/users/:id', {
|
|
131
|
+
schema: {
|
|
132
|
+
params: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: { id: { type: 'string' } },
|
|
135
|
+
required: ['id'],
|
|
136
|
+
},
|
|
137
|
+
response: {
|
|
138
|
+
200: { $ref: 'user#' },
|
|
139
|
+
404: { $ref: 'httpError#' },
|
|
140
|
+
500: { $ref: 'httpError#' },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}, handler);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Reply Helpers with @fastify/sensible
|
|
147
|
+
|
|
148
|
+
Use `@fastify/sensible` for standard HTTP errors:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import fastifySensible from '@fastify/sensible';
|
|
152
|
+
|
|
153
|
+
app.register(fastifySensible);
|
|
154
|
+
|
|
155
|
+
app.get('/users/:id', async (request, reply) => {
|
|
156
|
+
const user = await findUser(request.params.id);
|
|
157
|
+
if (!user) {
|
|
158
|
+
return reply.notFound('User not found');
|
|
159
|
+
}
|
|
160
|
+
if (!hasAccess(request.user, user)) {
|
|
161
|
+
return reply.forbidden('You cannot access this user');
|
|
162
|
+
}
|
|
163
|
+
return user;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Available methods:
|
|
167
|
+
// reply.badRequest(message?)
|
|
168
|
+
// reply.unauthorized(message?)
|
|
169
|
+
// reply.forbidden(message?)
|
|
170
|
+
// reply.notFound(message?)
|
|
171
|
+
// reply.methodNotAllowed(message?)
|
|
172
|
+
// reply.conflict(message?)
|
|
173
|
+
// reply.gone(message?)
|
|
174
|
+
// reply.unprocessableEntity(message?)
|
|
175
|
+
// reply.tooManyRequests(message?)
|
|
176
|
+
// reply.internalServerError(message?)
|
|
177
|
+
// reply.notImplemented(message?)
|
|
178
|
+
// reply.badGateway(message?)
|
|
179
|
+
// reply.serviceUnavailable(message?)
|
|
180
|
+
// reply.gatewayTimeout(message?)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Async Error Handling
|
|
184
|
+
|
|
185
|
+
Errors in async handlers are automatically caught:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// Errors are automatically caught and passed to error handler
|
|
189
|
+
app.get('/users', async (request) => {
|
|
190
|
+
const users = await db.users.findAll(); // If this throws, error handler catches it
|
|
191
|
+
return users;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Explicit error handling for custom logic
|
|
195
|
+
app.get('/users/:id', async (request, reply) => {
|
|
196
|
+
try {
|
|
197
|
+
const user = await db.users.findById(request.params.id);
|
|
198
|
+
if (!user) {
|
|
199
|
+
return reply.code(404).send({ error: 'User not found' });
|
|
200
|
+
}
|
|
201
|
+
return user;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Transform database errors
|
|
204
|
+
if (error.code === 'CONNECTION_ERROR') {
|
|
205
|
+
request.log.error({ err: error }, 'Database connection failed');
|
|
206
|
+
return reply.code(503).send({ error: 'Service temporarily unavailable' });
|
|
207
|
+
}
|
|
208
|
+
throw error; // Re-throw for error handler
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Hook Error Handling
|
|
214
|
+
|
|
215
|
+
Errors in hooks are handled the same way:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
219
|
+
const token = request.headers.authorization;
|
|
220
|
+
if (!token) {
|
|
221
|
+
// This error goes to the error handler
|
|
222
|
+
throw new UnauthorizedError();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
request.user = await verifyToken(token);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new UnauthorizedError();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Or use reply to send response directly
|
|
233
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
234
|
+
if (!request.headers.authorization) {
|
|
235
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
236
|
+
return; // Must return to stop processing
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Not Found Handler
|
|
242
|
+
|
|
243
|
+
Customize the 404 response:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
247
|
+
return reply.code(404).send({
|
|
248
|
+
statusCode: 404,
|
|
249
|
+
error: 'Not Found',
|
|
250
|
+
message: `Route ${request.method} ${request.url} not found`,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// With schema validation
|
|
255
|
+
app.setNotFoundHandler({
|
|
256
|
+
preValidation: async (request, reply) => {
|
|
257
|
+
// Pre-validation hook for 404 handler
|
|
258
|
+
},
|
|
259
|
+
}, async (request, reply) => {
|
|
260
|
+
return reply.code(404).send({ error: 'Not Found' });
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Error Wrapping
|
|
265
|
+
|
|
266
|
+
Wrap external errors with context:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import createError from '@fastify/error';
|
|
270
|
+
|
|
271
|
+
const DatabaseError = createError('DATABASE_ERROR', 'Database operation failed: %s', 500);
|
|
272
|
+
const ExternalServiceError = createError('EXTERNAL_SERVICE_ERROR', 'External service failed: %s', 502);
|
|
273
|
+
|
|
274
|
+
app.get('/users/:id', async (request) => {
|
|
275
|
+
try {
|
|
276
|
+
return await db.users.findById(request.params.id);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new DatabaseError(error.message, { cause: error });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
app.get('/weather', async (request) => {
|
|
283
|
+
try {
|
|
284
|
+
return await weatherApi.fetch(request.query.city);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw new ExternalServiceError(error.message, { cause: error });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Validation Error Customization
|
|
292
|
+
|
|
293
|
+
Customize validation error format:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
app.setErrorHandler((error, request, reply) => {
|
|
297
|
+
if (error.validation) {
|
|
298
|
+
const details = error.validation.map((err) => {
|
|
299
|
+
const field = err.instancePath
|
|
300
|
+
? err.instancePath.slice(1).replace(/\//g, '.')
|
|
301
|
+
: err.params?.missingProperty || 'unknown';
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
field,
|
|
305
|
+
message: err.message,
|
|
306
|
+
value: err.data,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return reply.code(400).send({
|
|
311
|
+
statusCode: 400,
|
|
312
|
+
error: 'Validation Error',
|
|
313
|
+
message: `Invalid ${error.validationContext}: ${details.map(d => d.field).join(', ')}`,
|
|
314
|
+
details,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Handle other errors...
|
|
319
|
+
throw error;
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Error Cause Chain
|
|
324
|
+
|
|
325
|
+
Preserve error chains for debugging:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
app.get('/complex-operation', async (request) => {
|
|
329
|
+
try {
|
|
330
|
+
await step1();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const wrapped = new Error('Step 1 failed', { cause: error });
|
|
333
|
+
wrapped.statusCode = 500;
|
|
334
|
+
throw wrapped;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// In error handler, log the full chain
|
|
339
|
+
app.setErrorHandler((error, request, reply) => {
|
|
340
|
+
// Log error with cause chain
|
|
341
|
+
let current = error;
|
|
342
|
+
const chain = [];
|
|
343
|
+
while (current) {
|
|
344
|
+
chain.push({
|
|
345
|
+
message: current.message,
|
|
346
|
+
code: current.code,
|
|
347
|
+
stack: current.stack,
|
|
348
|
+
});
|
|
349
|
+
current = current.cause;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
request.log.error({ errorChain: chain }, 'Request failed');
|
|
353
|
+
|
|
354
|
+
reply.code(error.statusCode || 500).send({
|
|
355
|
+
error: error.message,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Plugin-Scoped Error Handlers
|
|
361
|
+
|
|
362
|
+
Set error handlers at the plugin level:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
app.register(async function apiRoutes(fastify) {
|
|
366
|
+
// This error handler only applies to routes in this plugin
|
|
367
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
368
|
+
request.log.error({ err: error }, 'API error');
|
|
369
|
+
|
|
370
|
+
reply.code(error.statusCode || 500).send({
|
|
371
|
+
error: {
|
|
372
|
+
code: error.code || 'API_ERROR',
|
|
373
|
+
message: error.message,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
fastify.get('/data', async () => {
|
|
379
|
+
throw new Error('API-specific error');
|
|
380
|
+
});
|
|
381
|
+
}, { prefix: '/api' });
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Graceful Error Recovery
|
|
385
|
+
|
|
386
|
+
Handle errors gracefully without crashing:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
app.get('/resilient', async (request, reply) => {
|
|
390
|
+
const results = await Promise.allSettled([
|
|
391
|
+
fetchPrimaryData(),
|
|
392
|
+
fetchSecondaryData(),
|
|
393
|
+
fetchOptionalData(),
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const [primary, secondary, optional] = results;
|
|
397
|
+
|
|
398
|
+
if (primary.status === 'rejected') {
|
|
399
|
+
// Primary data is required
|
|
400
|
+
throw new Error('Primary data unavailable');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
data: primary.value,
|
|
405
|
+
secondary: secondary.status === 'fulfilled' ? secondary.value : null,
|
|
406
|
+
optional: optional.status === 'fulfilled' ? optional.value : null,
|
|
407
|
+
warnings: results
|
|
408
|
+
.filter((r) => r.status === 'rejected')
|
|
409
|
+
.map((r) => r.reason.message),
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
```
|