te.js 2.1.0 → 2.1.2

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.
Files changed (70) hide show
  1. package/README.md +197 -196
  2. package/auto-docs/analysis/handler-analyzer.js +58 -58
  3. package/auto-docs/analysis/source-resolver.js +101 -101
  4. package/auto-docs/constants.js +37 -37
  5. package/auto-docs/docs-llm/index.js +7 -7
  6. package/auto-docs/docs-llm/prompts.js +222 -222
  7. package/auto-docs/docs-llm/provider.js +132 -132
  8. package/auto-docs/index.js +146 -146
  9. package/auto-docs/openapi/endpoint-processor.js +277 -277
  10. package/auto-docs/openapi/generator.js +107 -107
  11. package/auto-docs/openapi/level3.js +131 -131
  12. package/auto-docs/openapi/spec-builders.js +244 -244
  13. package/auto-docs/ui/docs-ui.js +186 -186
  14. package/auto-docs/utils/logger.js +17 -17
  15. package/auto-docs/utils/strip-usage.js +10 -10
  16. package/cli/docs-command.js +315 -315
  17. package/cli/fly-command.js +71 -71
  18. package/cli/index.js +56 -56
  19. package/cors/index.js +71 -0
  20. package/database/index.js +165 -165
  21. package/database/mongodb.js +146 -146
  22. package/database/redis.js +201 -201
  23. package/docs/README.md +36 -36
  24. package/docs/ammo.md +362 -362
  25. package/docs/api-reference.md +490 -490
  26. package/docs/auto-docs.md +216 -216
  27. package/docs/cli.md +152 -152
  28. package/docs/configuration.md +275 -275
  29. package/docs/database.md +390 -390
  30. package/docs/error-handling.md +438 -438
  31. package/docs/file-uploads.md +333 -333
  32. package/docs/getting-started.md +214 -214
  33. package/docs/middleware.md +355 -355
  34. package/docs/rate-limiting.md +393 -393
  35. package/docs/routing.md +302 -302
  36. package/lib/llm/client.js +73 -0
  37. package/lib/llm/index.js +7 -0
  38. package/lib/llm/parse.js +89 -0
  39. package/package.json +64 -62
  40. package/rate-limit/algorithms/fixed-window.js +141 -141
  41. package/rate-limit/algorithms/sliding-window.js +147 -147
  42. package/rate-limit/algorithms/token-bucket.js +115 -115
  43. package/rate-limit/base.js +165 -165
  44. package/rate-limit/index.js +147 -147
  45. package/rate-limit/storage/base.js +104 -104
  46. package/rate-limit/storage/memory.js +101 -101
  47. package/rate-limit/storage/redis.js +88 -88
  48. package/server/ammo/body-parser.js +220 -220
  49. package/server/ammo/dispatch-helper.js +103 -103
  50. package/server/ammo/enhancer.js +57 -57
  51. package/server/ammo.js +454 -415
  52. package/server/endpoint.js +97 -74
  53. package/server/error.js +9 -9
  54. package/server/errors/code-context.js +125 -125
  55. package/server/errors/llm-error-service.js +140 -140
  56. package/server/files/helper.js +33 -33
  57. package/server/files/uploader.js +143 -143
  58. package/server/handler.js +158 -119
  59. package/server/target.js +185 -175
  60. package/server/targets/middleware-validator.js +22 -22
  61. package/server/targets/path-validator.js +21 -21
  62. package/server/targets/registry.js +160 -160
  63. package/server/targets/shoot-validator.js +21 -21
  64. package/te.js +428 -402
  65. package/utils/auto-register.js +17 -17
  66. package/utils/configuration.js +64 -64
  67. package/utils/errors-llm-config.js +84 -84
  68. package/utils/request-logger.js +43 -43
  69. package/utils/status-codes.js +82 -82
  70. package/utils/tejas-entrypoint-html.js +18 -18
@@ -1,438 +1,438 @@
1
- # Error Handling
2
-
3
- Tejas keeps your application from crashing on unhandled errors. You don't log the error and send the response separately — **`ammo.throw()` is the single mechanism**: it sends the appropriate HTTP response (logging is optional via `log.exceptions`). Whether you call `ammo.throw()` or the framework calls it when it catches an error, the same behaviour applies. When LLM-inferred errors are enabled, call `ammo.throw()` with no arguments and an LLM infers status and message from code context; explicit code and message always override.
4
-
5
- ## Zero-Config Error Handling
6
-
7
- **One of Tejas's most powerful features is that you don't need to write any error handling code** — the framework catches all errors automatically at multiple levels.
8
-
9
- ### How It Works
10
-
11
- Tejas wraps all middleware and route handlers with built-in error catching. Any error thrown in your code is automatically passed to `ammo.throw(err)` — the same mechanism you use for intentional errors. So: one place handles everything (response + optional logging via `log.exceptions`). No separate "log then send response"; your app never crashes and clients always receive a proper response.
12
-
13
- ### Write Clean Code Without Try-Catch
14
-
15
- ```javascript
16
- // ✅ No try-catch needed — Tejas handles errors automatically
17
- target.register('/users/:id', async (ammo) => {
18
- const user = await database.findUser(ammo.payload.id); // If this throws, Tejas catches it
19
- const posts = await database.getUserPosts(user.id); // Same here
20
- ammo.fire({ user, posts });
21
- });
22
- ```
23
-
24
- In other frameworks you typically **log the error and then send the response** (two separate steps). With Tejas, **`ammo.throw()` does both** — and when the framework catches an error it uses the same `ammo.throw()`, so you never define them separately:
25
-
26
- ```javascript
27
- // ❌ Traditional: log then send response (two separate things)
28
- app.get('/users/:id', async (req, res) => {
29
- try {
30
- const user = await database.findUser(req.params.id);
31
- res.json(user);
32
- } catch (error) {
33
- console.error(error); // 1. log
34
- res.status(500).json({ error: 'Internal Server Error' }); // 2. send response
35
- }
36
- });
37
- ```
38
-
39
- ### Automatic Error Responses
40
-
41
- When an unhandled error occurs, the framework calls `ammo.throw(err)` — the same method you use for intentional errors. So one mechanism: explicit `ammo.throw()` or framework-caught, both go through `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, status and message are inferred from code context; otherwise or when you pass explicit code/message, those are used.
42
-
43
- ### Enable Error Logging
44
-
45
- To see caught exceptions in your logs, enable exception logging:
46
-
47
- ```javascript
48
- const app = new Tejas({
49
- log: {
50
- exceptions: true // Log all caught exceptions
51
- }
52
- });
53
- ```
54
-
55
- Or via environment variable:
56
-
57
- ```bash
58
- LOG_EXCEPTIONS=true
59
- ```
60
-
61
- ---
62
-
63
- ## LLM-Inferred Errors
64
-
65
- When **`errors.llm.enabled`** is true and you call `ammo.throw()` without an explicit status code or message, Tejas uses an LLM to infer an appropriate HTTP status code and message from **code context** — you do not pass an error object. The framework captures the code surrounding the `ammo.throw()` call (with line numbers) and all **upstream** (callers) and **downstream** (code that would have run next) context, and the LLM infers what went wrong from that. Explicit code and message always override.
66
-
67
- - **No error object required:** Call `ammo.throw()` with no arguments (or only options). The LLM receives the source code around the call site and upstream call stacks so it can infer status and message from control flow and intent.
68
- - **Opt-in:** Enable via config: `errors.llm.enabled: true` and configure `errors.llm` (baseURL, apiKey, model), or call **`app.withLLMErrors()`** / **`app.withLLMErrors({ baseURL, apiKey, model, messageType })`** before `takeoff()`. See [Configuration](./configuration.md#error-handling-llm-inferred-errors).
69
- - **Framework-caught errors:** When the framework catches an unhandled error (in a handler or middleware), it uses the same `ammo.throw(err)` — so the same `errors.llm` config applies. No separate "log then send response"; one mechanism handles everything.
70
- - **Override:** Whenever you pass a status code or message (e.g. `ammo.throw(404, 'User not found')` or `throw new TejError(404, 'User not found')`), that value is used; the LLM is not called.
71
- - **Message type:** Configure whether the LLM generates **end-user-friendly** or **developer-friendly** messages via `errors.llm.messageType`; override per call (see [Per-call overrides](#per-call-overrides)).
72
- - **Non-production:** In non-production, the LLM can also provide developer insight (e.g. bug vs environment, suggested fix), attached to the response as `_dev` or in logs only — never in production.
73
-
74
- ### Per-call overrides
75
-
76
- For any LLM-eligible `ammo.throw()` call (no explicit status code), you can pass an options object as the last argument to override behaviour for that call only:
77
-
78
- - **`useLlm`** (boolean): Set to `false` to skip the LLM for this call and respond with a default 500 / "Internal Server Error" (or the error's message when you pass an Error/string). Set to `true` to force using the LLM (same as default when eligible).
79
- - **`messageType`** (`"endUser"` | `"developer"`): Override the configured default for this call — request an end-user-friendly or developer-friendly message.
80
-
81
- ```javascript
82
- // Skip LLM for this call; send 500
83
- ammo.throw({ useLlm: false });
84
-
85
- // Request a developer-friendly message for this call only
86
- ammo.throw({ messageType: 'developer' });
87
-
88
- // When an error was caught and passed in, you can still pass options
89
- ammo.throw(caughtErr, { useLlm: false });
90
- ```
91
-
92
- ---
93
-
94
- ## TejError Class
95
-
96
- Use `TejError` for throwing HTTP errors with status codes. Both status code and message are **optional** when [LLM-inferred errors](#llm-inferred-errors) are enabled and the error is passed through `ammo.throw()`; otherwise, supply them to set the response explicitly.
97
-
98
- ```javascript
99
- import { TejError } from 'te.js';
100
-
101
- // Explicit code and message (always used as override)
102
- throw new TejError(404, 'User not found');
103
- throw new TejError(400, 'Invalid email format');
104
- throw new TejError(500, 'Database connection failed');
105
- ```
106
-
107
- ## Error Response
108
-
109
- When an error is thrown, Tejas automatically sends the appropriate HTTP response:
110
-
111
- ```javascript
112
- throw new TejError(404, 'Resource not found');
113
- ```
114
-
115
- **Response:**
116
- ```
117
- HTTP/1.1 404 Not Found
118
- Content-Type: text/plain
119
-
120
- Resource not found
121
- ```
122
-
123
- ## Convenience Methods
124
-
125
- `Ammo` provides shortcut methods for common errors:
126
-
127
- ```javascript
128
- // 404 Not Found
129
- ammo.notFound();
130
-
131
- // 405 Method Not Allowed
132
- ammo.notAllowed();
133
-
134
- // 401 Unauthorized
135
- ammo.unauthorized();
136
- ```
137
-
138
- ## Using ammo.throw()
139
-
140
- For more control, use `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, you can omit code and message and the LLM will infer them; otherwise, or when you want to override, pass them explicitly.
141
-
142
- ```javascript
143
- // Explicit: status code and/or message
144
- ammo.throw(404);
145
- ammo.throw(404, 'User not found');
146
- ammo.throw(new TejError(400, 'Bad request'));
147
-
148
- // When errors.llm.enabled: LLM infers code and message from context
149
- ammo.throw(new Error('Something went wrong'));
150
- ammo.throw('Validation failed');
151
- ammo.throw(); // context still used when available
152
- ```
153
-
154
- See [Ammo — throw()](./ammo.md#throw--send-error-response) for all signatures and the LLM-inferred row.
155
-
156
- ## Error Handling in Routes
157
-
158
- ### Basic Pattern
159
-
160
- ```javascript
161
- target.register('/users/:id', async (ammo) => {
162
- const { id } = ammo.payload;
163
-
164
- const user = await findUser(id);
165
-
166
- if (!user) {
167
- throw new TejError(404, 'User not found');
168
- }
169
-
170
- ammo.fire(user);
171
- });
172
- ```
173
-
174
- ### Try-Catch Pattern
175
-
176
- ```javascript
177
- target.register('/data', async (ammo) => {
178
- try {
179
- const data = await riskyOperation();
180
- ammo.fire(data);
181
- } catch (error) {
182
- if (error.code === 'TIMEOUT') {
183
- throw new TejError(504, 'Gateway timeout');
184
- }
185
- throw new TejError(500, 'Internal server error');
186
- }
187
- });
188
- ```
189
-
190
- ## Global Error Handling
191
-
192
- Errors are automatically caught by Tejas's handler. Enable logging:
193
-
194
- ```javascript
195
- const app = new Tejas({
196
- log: {
197
- exceptions: true // Log all exceptions
198
- }
199
- });
200
- ```
201
-
202
- ## Custom Error Middleware
203
-
204
- Create middleware to customize error handling:
205
-
206
- ```javascript
207
- // middleware/error-handler.js
208
- export const errorHandler = (ammo, next) => {
209
- const originalThrow = ammo.throw.bind(ammo);
210
-
211
- ammo.throw = (...args) => {
212
- // Log errors
213
- console.error('Error:', args);
214
-
215
- // Send to error tracking service
216
- errorTracker.capture(args[0]);
217
-
218
- // Call original throw
219
- originalThrow(...args);
220
- };
221
-
222
- next();
223
- };
224
-
225
- // Apply globally
226
- app.midair(errorHandler);
227
- ```
228
-
229
- ## Structured Error Responses
230
-
231
- For APIs, return structured error objects:
232
-
233
- ```javascript
234
- // middleware/api-errors.js
235
- export const apiErrorHandler = (ammo, next) => {
236
- const originalThrow = ammo.throw.bind(ammo);
237
-
238
- ammo.throw = (statusOrError, message) => {
239
- let status = 500;
240
- let errorMessage = 'Internal Server Error';
241
- let errorCode = 'INTERNAL_ERROR';
242
-
243
- if (typeof statusOrError === 'number') {
244
- status = statusOrError;
245
- errorMessage = message || getDefaultMessage(status);
246
- errorCode = getErrorCode(status);
247
- } else if (statusOrError instanceof TejError) {
248
- status = statusOrError.code;
249
- errorMessage = statusOrError.message;
250
- errorCode = getErrorCode(status);
251
- }
252
-
253
- ammo.fire(status, {
254
- error: {
255
- code: errorCode,
256
- message: errorMessage,
257
- status
258
- }
259
- });
260
- };
261
-
262
- next();
263
- };
264
-
265
- function getDefaultMessage(status) {
266
- const messages = {
267
- 400: 'Bad Request',
268
- 401: 'Unauthorized',
269
- 403: 'Forbidden',
270
- 404: 'Not Found',
271
- 405: 'Method Not Allowed',
272
- 429: 'Too Many Requests',
273
- 500: 'Internal Server Error'
274
- };
275
- return messages[status] || 'Unknown Error';
276
- }
277
-
278
- function getErrorCode(status) {
279
- const codes = {
280
- 400: 'BAD_REQUEST',
281
- 401: 'UNAUTHORIZED',
282
- 403: 'FORBIDDEN',
283
- 404: 'NOT_FOUND',
284
- 405: 'METHOD_NOT_ALLOWED',
285
- 429: 'RATE_LIMITED',
286
- 500: 'INTERNAL_ERROR'
287
- };
288
- return codes[status] || 'UNKNOWN_ERROR';
289
- }
290
- ```
291
-
292
- **Response:**
293
- ```json
294
- {
295
- "error": {
296
- "code": "NOT_FOUND",
297
- "message": "User not found",
298
- "status": 404
299
- }
300
- }
301
- ```
302
-
303
- ## Validation Errors
304
-
305
- For input validation, return detailed errors:
306
-
307
- ```javascript
308
- target.register('/users', (ammo) => {
309
- if (!ammo.POST) return ammo.notAllowed();
310
-
311
- const { name, email, age } = ammo.payload;
312
- const errors = [];
313
-
314
- if (!name) errors.push({ field: 'name', message: 'Name is required' });
315
- if (!email) errors.push({ field: 'email', message: 'Email is required' });
316
- if (email && !isValidEmail(email)) {
317
- errors.push({ field: 'email', message: 'Invalid email format' });
318
- }
319
- if (age && (isNaN(age) || age < 0)) {
320
- errors.push({ field: 'age', message: 'Age must be a positive number' });
321
- }
322
-
323
- if (errors.length > 0) {
324
- return ammo.fire(400, {
325
- error: {
326
- code: 'VALIDATION_ERROR',
327
- message: 'Validation failed',
328
- details: errors
329
- }
330
- });
331
- }
332
-
333
- // Process valid data...
334
- });
335
- ```
336
-
337
- ## Async Error Handling
338
-
339
- Tejas automatically catches errors in **both sync and async handlers** — including Promise rejections:
340
-
341
- ```javascript
342
- // ✅ No try-catch needed — errors are caught automatically
343
- target.register('/async', async (ammo) => {
344
- const data = await fetchData(); // If this throws, Tejas catches it
345
- ammo.fire(data);
346
- });
347
-
348
- // ✅ Multiple await calls? Still no try-catch needed
349
- target.register('/complex', async (ammo) => {
350
- const user = await getUser(ammo.payload.id);
351
- const profile = await getProfile(user.profileId);
352
- const settings = await getSettings(user.id);
353
- ammo.fire({ user, profile, settings });
354
- });
355
-
356
- // 🔧 Use try-catch ONLY when you need custom error handling
357
- target.register('/async-custom', async (ammo) => {
358
- try {
359
- const data = await fetchData();
360
- ammo.fire(data);
361
- } catch (error) {
362
- if (error.code === 'ECONNREFUSED') {
363
- throw new TejError(503, 'Service temporarily unavailable');
364
- }
365
- throw error; // Re-throw unknown errors (Tejas will still catch it)
366
- }
367
- });
368
- ```
369
-
370
- ### When You Still Might Want Try-Catch
371
-
372
- While Tejas catches all errors automatically, you may want try-catch for:
373
-
374
- 1. **Custom error mapping** — Convert database errors to user-friendly messages
375
- 2. **Retry logic** — Attempt an operation multiple times before failing
376
- 3. **Cleanup operations** — Release resources even on failure
377
- 4. **Partial success** — Continue processing after non-critical failures
378
-
379
- ## BodyParserError
380
-
381
- `BodyParserError` is a subclass of `TejError` thrown automatically during request body parsing. You do not need to handle these yourself — they are caught by the framework and converted to appropriate HTTP responses.
382
-
383
- | Status | Condition |
384
- |--------|-----------|
385
- | **400** | Malformed JSON, invalid URL-encoded data, or corrupted multipart form data |
386
- | **408** | Body parsing timed out (exceeds `body.timeout`, default 30 seconds) |
387
- | **413** | Request body exceeds `body.max_size` (default 10 MB) |
388
- | **415** | Unsupported content type (not JSON, URL-encoded, or multipart) |
389
-
390
- These limits are configured via [Configuration](./configuration.md) (`body.max_size`, `body.timeout`).
391
-
392
- Supported content types:
393
- - `application/json`
394
- - `application/x-www-form-urlencoded`
395
- - `multipart/form-data`
396
-
397
- ## Error Flow
398
-
399
- When any error occurs in your handler or middleware, the framework uses the **same** `ammo.throw(err)` you use for intentional errors — one mechanism:
400
-
401
- 1. The framework's `executeChain()` catches the error
402
- 2. If `LOG_EXCEPTIONS` is enabled, the error is logged
403
- 3. The error is passed to `ammo.throw(err)` (no separate "send response" step — `ammo.throw()` does it)
404
- 4. **TejError** — uses the error's `code` and `message` directly
405
- 5. **When errors.llm.enabled** — LLM infers status and message from code context (same as explicit `ammo.throw()`)
406
- 6. **Otherwise** — 500 with the error message or string representation
407
- 7. `ammo.throw()` sends the HTTP response via `ammo.fire(statusCode, message)`
408
-
409
- Once a response has been sent (`res.headersSent` is true), no further middleware or handlers execute.
410
-
411
- ## Error Codes Reference
412
-
413
- | Status | Name | When to Use |
414
- |--------|------|-------------|
415
- | 400 | Bad Request | Invalid input, malformed request |
416
- | 401 | Unauthorized | Missing or invalid authentication |
417
- | 403 | Forbidden | Authenticated but not authorized |
418
- | 404 | Not Found | Resource doesn't exist |
419
- | 405 | Method Not Allowed | HTTP method not supported |
420
- | 409 | Conflict | Resource conflict (duplicate) |
421
- | 413 | Payload Too Large | Request body too large |
422
- | 422 | Unprocessable Entity | Valid syntax but semantic errors |
423
- | 429 | Too Many Requests | Rate limit exceeded |
424
- | 500 | Internal Server Error | Unexpected server errors |
425
- | 502 | Bad Gateway | Upstream server error |
426
- | 503 | Service Unavailable | Server temporarily unavailable |
427
- | 504 | Gateway Timeout | Upstream server timeout |
428
-
429
- ## Best Practices
430
-
431
- 1. **Use appropriate status codes** — Don't return 500 for client errors
432
- 2. **Provide useful messages** — Help developers debug issues
433
- 3. **Don't expose internals** — Hide stack traces in production
434
- 4. **Log errors** — Enable exception logging for debugging
435
- 5. **Be consistent** — Use the same error format throughout your API
436
- 6. **Validate early** — Check input before processing
437
- 7. **Use TejError or ammo.throw(code, message)** — For HTTP-specific errors when you want explicit control
438
- 8. **Opt in to LLM-inferred errors when helpful** — Enable via `errors.llm.enabled` in config or **`app.withLLMErrors(config?)`** before `takeoff()`, then configure baseURL, apiKey, and model so you can throw without specifying code or message and let the LLM infer them; see [Configuration](./configuration.md#error-handling-llm-inferred-errors)
1
+ # Error Handling
2
+
3
+ Tejas keeps your application from crashing on unhandled errors. You don't log the error and send the response separately — **`ammo.throw()` is the single mechanism**: it sends the appropriate HTTP response (logging is optional via `log.exceptions`). Whether you call `ammo.throw()` or the framework calls it when it catches an error, the same behaviour applies. When LLM-inferred errors are enabled, call `ammo.throw()` with no arguments and an LLM infers status and message from code context; explicit code and message always override.
4
+
5
+ ## Zero-Config Error Handling
6
+
7
+ **One of Tejas's most powerful features is that you don't need to write any error handling code** — the framework catches all errors automatically at multiple levels.
8
+
9
+ ### How It Works
10
+
11
+ Tejas wraps all middleware and route handlers with built-in error catching. Any error thrown in your code is automatically passed to `ammo.throw(err)` — the same mechanism you use for intentional errors. So: one place handles everything (response + optional logging via `log.exceptions`). No separate "log then send response"; your app never crashes and clients always receive a proper response.
12
+
13
+ ### Write Clean Code Without Try-Catch
14
+
15
+ ```javascript
16
+ // ✅ No try-catch needed — Tejas handles errors automatically
17
+ target.register('/users/:id', async (ammo) => {
18
+ const user = await database.findUser(ammo.payload.id); // If this throws, Tejas catches it
19
+ const posts = await database.getUserPosts(user.id); // Same here
20
+ ammo.fire({ user, posts });
21
+ });
22
+ ```
23
+
24
+ In other frameworks you typically **log the error and then send the response** (two separate steps). With Tejas, **`ammo.throw()` does both** — and when the framework catches an error it uses the same `ammo.throw()`, so you never define them separately:
25
+
26
+ ```javascript
27
+ // ❌ Traditional: log then send response (two separate things)
28
+ app.get('/users/:id', async (req, res) => {
29
+ try {
30
+ const user = await database.findUser(req.params.id);
31
+ res.json(user);
32
+ } catch (error) {
33
+ console.error(error); // 1. log
34
+ res.status(500).json({ error: 'Internal Server Error' }); // 2. send response
35
+ }
36
+ });
37
+ ```
38
+
39
+ ### Automatic Error Responses
40
+
41
+ When an unhandled error occurs, the framework calls `ammo.throw(err)` — the same method you use for intentional errors. So one mechanism: explicit `ammo.throw()` or framework-caught, both go through `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, status and message are inferred from code context; otherwise or when you pass explicit code/message, those are used.
42
+
43
+ ### Enable Error Logging
44
+
45
+ To see caught exceptions in your logs, enable exception logging:
46
+
47
+ ```javascript
48
+ const app = new Tejas({
49
+ log: {
50
+ exceptions: true // Log all caught exceptions
51
+ }
52
+ });
53
+ ```
54
+
55
+ Or via environment variable:
56
+
57
+ ```bash
58
+ LOG_EXCEPTIONS=true
59
+ ```
60
+
61
+ ---
62
+
63
+ ## LLM-Inferred Errors
64
+
65
+ When **`errors.llm.enabled`** is true and you call `ammo.throw()` without an explicit status code or message, Tejas uses an LLM to infer an appropriate HTTP status code and message from **code context** — you do not pass an error object. The framework captures the code surrounding the `ammo.throw()` call (with line numbers) and all **upstream** (callers) and **downstream** (code that would have run next) context, and the LLM infers what went wrong from that. Explicit code and message always override.
66
+
67
+ - **No error object required:** Call `ammo.throw()` with no arguments (or only options). The LLM receives the source code around the call site and upstream call stacks so it can infer status and message from control flow and intent.
68
+ - **Opt-in:** Enable via config: `errors.llm.enabled: true` and configure `errors.llm` (baseURL, apiKey, model), or call **`app.withLLMErrors()`** / **`app.withLLMErrors({ baseURL, apiKey, model, messageType })`** before `takeoff()`. See [Configuration](./configuration.md#error-handling-llm-inferred-errors).
69
+ - **Framework-caught errors:** When the framework catches an unhandled error (in a handler or middleware), it uses the same `ammo.throw(err)` — so the same `errors.llm` config applies. No separate "log then send response"; one mechanism handles everything.
70
+ - **Override:** Whenever you pass a status code or message (e.g. `ammo.throw(404, 'User not found')` or `throw new TejError(404, 'User not found')`), that value is used; the LLM is not called.
71
+ - **Message type:** Configure whether the LLM generates **end-user-friendly** or **developer-friendly** messages via `errors.llm.messageType`; override per call (see [Per-call overrides](#per-call-overrides)).
72
+ - **Non-production:** In non-production, the LLM can also provide developer insight (e.g. bug vs environment, suggested fix), attached to the response as `_dev` or in logs only — never in production.
73
+
74
+ ### Per-call overrides
75
+
76
+ For any LLM-eligible `ammo.throw()` call (no explicit status code), you can pass an options object as the last argument to override behaviour for that call only:
77
+
78
+ - **`useLlm`** (boolean): Set to `false` to skip the LLM for this call and respond with a default 500 / "Internal Server Error" (or the error's message when you pass an Error/string). Set to `true` to force using the LLM (same as default when eligible).
79
+ - **`messageType`** (`"endUser"` | `"developer"`): Override the configured default for this call — request an end-user-friendly or developer-friendly message.
80
+
81
+ ```javascript
82
+ // Skip LLM for this call; send 500
83
+ ammo.throw({ useLlm: false });
84
+
85
+ // Request a developer-friendly message for this call only
86
+ ammo.throw({ messageType: 'developer' });
87
+
88
+ // When an error was caught and passed in, you can still pass options
89
+ ammo.throw(caughtErr, { useLlm: false });
90
+ ```
91
+
92
+ ---
93
+
94
+ ## TejError Class
95
+
96
+ Use `TejError` for throwing HTTP errors with status codes. Both status code and message are **optional** when [LLM-inferred errors](#llm-inferred-errors) are enabled and the error is passed through `ammo.throw()`; otherwise, supply them to set the response explicitly.
97
+
98
+ ```javascript
99
+ import { TejError } from 'te.js';
100
+
101
+ // Explicit code and message (always used as override)
102
+ throw new TejError(404, 'User not found');
103
+ throw new TejError(400, 'Invalid email format');
104
+ throw new TejError(500, 'Database connection failed');
105
+ ```
106
+
107
+ ## Error Response
108
+
109
+ When an error is thrown, Tejas automatically sends the appropriate HTTP response:
110
+
111
+ ```javascript
112
+ throw new TejError(404, 'Resource not found');
113
+ ```
114
+
115
+ **Response:**
116
+ ```
117
+ HTTP/1.1 404 Not Found
118
+ Content-Type: text/plain
119
+
120
+ Resource not found
121
+ ```
122
+
123
+ ## Convenience Methods
124
+
125
+ `Ammo` provides shortcut methods for common errors:
126
+
127
+ ```javascript
128
+ // 404 Not Found
129
+ ammo.notFound();
130
+
131
+ // 405 Method Not Allowed
132
+ ammo.notAllowed();
133
+
134
+ // 401 Unauthorized
135
+ ammo.unauthorized();
136
+ ```
137
+
138
+ ## Using ammo.throw()
139
+
140
+ For more control, use `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, you can omit code and message and the LLM will infer them; otherwise, or when you want to override, pass them explicitly.
141
+
142
+ ```javascript
143
+ // Explicit: status code and/or message
144
+ ammo.throw(404);
145
+ ammo.throw(404, 'User not found');
146
+ ammo.throw(new TejError(400, 'Bad request'));
147
+
148
+ // When errors.llm.enabled: LLM infers code and message from context
149
+ ammo.throw(new Error('Something went wrong'));
150
+ ammo.throw('Validation failed');
151
+ ammo.throw(); // context still used when available
152
+ ```
153
+
154
+ See [Ammo — throw()](./ammo.md#throw--send-error-response) for all signatures and the LLM-inferred row.
155
+
156
+ ## Error Handling in Routes
157
+
158
+ ### Basic Pattern
159
+
160
+ ```javascript
161
+ target.register('/users/:id', async (ammo) => {
162
+ const { id } = ammo.payload;
163
+
164
+ const user = await findUser(id);
165
+
166
+ if (!user) {
167
+ throw new TejError(404, 'User not found');
168
+ }
169
+
170
+ ammo.fire(user);
171
+ });
172
+ ```
173
+
174
+ ### Try-Catch Pattern
175
+
176
+ ```javascript
177
+ target.register('/data', async (ammo) => {
178
+ try {
179
+ const data = await riskyOperation();
180
+ ammo.fire(data);
181
+ } catch (error) {
182
+ if (error.code === 'TIMEOUT') {
183
+ throw new TejError(504, 'Gateway timeout');
184
+ }
185
+ throw new TejError(500, 'Internal server error');
186
+ }
187
+ });
188
+ ```
189
+
190
+ ## Global Error Handling
191
+
192
+ Errors are automatically caught by Tejas's handler. Enable logging:
193
+
194
+ ```javascript
195
+ const app = new Tejas({
196
+ log: {
197
+ exceptions: true // Log all exceptions
198
+ }
199
+ });
200
+ ```
201
+
202
+ ## Custom Error Middleware
203
+
204
+ Create middleware to customize error handling:
205
+
206
+ ```javascript
207
+ // middleware/error-handler.js
208
+ export const errorHandler = (ammo, next) => {
209
+ const originalThrow = ammo.throw.bind(ammo);
210
+
211
+ ammo.throw = (...args) => {
212
+ // Log errors
213
+ console.error('Error:', args);
214
+
215
+ // Send to error tracking service
216
+ errorTracker.capture(args[0]);
217
+
218
+ // Call original throw
219
+ originalThrow(...args);
220
+ };
221
+
222
+ next();
223
+ };
224
+
225
+ // Apply globally
226
+ app.midair(errorHandler);
227
+ ```
228
+
229
+ ## Structured Error Responses
230
+
231
+ For APIs, return structured error objects:
232
+
233
+ ```javascript
234
+ // middleware/api-errors.js
235
+ export const apiErrorHandler = (ammo, next) => {
236
+ const originalThrow = ammo.throw.bind(ammo);
237
+
238
+ ammo.throw = (statusOrError, message) => {
239
+ let status = 500;
240
+ let errorMessage = 'Internal Server Error';
241
+ let errorCode = 'INTERNAL_ERROR';
242
+
243
+ if (typeof statusOrError === 'number') {
244
+ status = statusOrError;
245
+ errorMessage = message || getDefaultMessage(status);
246
+ errorCode = getErrorCode(status);
247
+ } else if (statusOrError instanceof TejError) {
248
+ status = statusOrError.code;
249
+ errorMessage = statusOrError.message;
250
+ errorCode = getErrorCode(status);
251
+ }
252
+
253
+ ammo.fire(status, {
254
+ error: {
255
+ code: errorCode,
256
+ message: errorMessage,
257
+ status
258
+ }
259
+ });
260
+ };
261
+
262
+ next();
263
+ };
264
+
265
+ function getDefaultMessage(status) {
266
+ const messages = {
267
+ 400: 'Bad Request',
268
+ 401: 'Unauthorized',
269
+ 403: 'Forbidden',
270
+ 404: 'Not Found',
271
+ 405: 'Method Not Allowed',
272
+ 429: 'Too Many Requests',
273
+ 500: 'Internal Server Error'
274
+ };
275
+ return messages[status] || 'Unknown Error';
276
+ }
277
+
278
+ function getErrorCode(status) {
279
+ const codes = {
280
+ 400: 'BAD_REQUEST',
281
+ 401: 'UNAUTHORIZED',
282
+ 403: 'FORBIDDEN',
283
+ 404: 'NOT_FOUND',
284
+ 405: 'METHOD_NOT_ALLOWED',
285
+ 429: 'RATE_LIMITED',
286
+ 500: 'INTERNAL_ERROR'
287
+ };
288
+ return codes[status] || 'UNKNOWN_ERROR';
289
+ }
290
+ ```
291
+
292
+ **Response:**
293
+ ```json
294
+ {
295
+ "error": {
296
+ "code": "NOT_FOUND",
297
+ "message": "User not found",
298
+ "status": 404
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Validation Errors
304
+
305
+ For input validation, return detailed errors:
306
+
307
+ ```javascript
308
+ target.register('/users', (ammo) => {
309
+ if (!ammo.POST) return ammo.notAllowed();
310
+
311
+ const { name, email, age } = ammo.payload;
312
+ const errors = [];
313
+
314
+ if (!name) errors.push({ field: 'name', message: 'Name is required' });
315
+ if (!email) errors.push({ field: 'email', message: 'Email is required' });
316
+ if (email && !isValidEmail(email)) {
317
+ errors.push({ field: 'email', message: 'Invalid email format' });
318
+ }
319
+ if (age && (isNaN(age) || age < 0)) {
320
+ errors.push({ field: 'age', message: 'Age must be a positive number' });
321
+ }
322
+
323
+ if (errors.length > 0) {
324
+ return ammo.fire(400, {
325
+ error: {
326
+ code: 'VALIDATION_ERROR',
327
+ message: 'Validation failed',
328
+ details: errors
329
+ }
330
+ });
331
+ }
332
+
333
+ // Process valid data...
334
+ });
335
+ ```
336
+
337
+ ## Async Error Handling
338
+
339
+ Tejas automatically catches errors in **both sync and async handlers** — including Promise rejections:
340
+
341
+ ```javascript
342
+ // ✅ No try-catch needed — errors are caught automatically
343
+ target.register('/async', async (ammo) => {
344
+ const data = await fetchData(); // If this throws, Tejas catches it
345
+ ammo.fire(data);
346
+ });
347
+
348
+ // ✅ Multiple await calls? Still no try-catch needed
349
+ target.register('/complex', async (ammo) => {
350
+ const user = await getUser(ammo.payload.id);
351
+ const profile = await getProfile(user.profileId);
352
+ const settings = await getSettings(user.id);
353
+ ammo.fire({ user, profile, settings });
354
+ });
355
+
356
+ // 🔧 Use try-catch ONLY when you need custom error handling
357
+ target.register('/async-custom', async (ammo) => {
358
+ try {
359
+ const data = await fetchData();
360
+ ammo.fire(data);
361
+ } catch (error) {
362
+ if (error.code === 'ECONNREFUSED') {
363
+ throw new TejError(503, 'Service temporarily unavailable');
364
+ }
365
+ throw error; // Re-throw unknown errors (Tejas will still catch it)
366
+ }
367
+ });
368
+ ```
369
+
370
+ ### When You Still Might Want Try-Catch
371
+
372
+ While Tejas catches all errors automatically, you may want try-catch for:
373
+
374
+ 1. **Custom error mapping** — Convert database errors to user-friendly messages
375
+ 2. **Retry logic** — Attempt an operation multiple times before failing
376
+ 3. **Cleanup operations** — Release resources even on failure
377
+ 4. **Partial success** — Continue processing after non-critical failures
378
+
379
+ ## BodyParserError
380
+
381
+ `BodyParserError` is a subclass of `TejError` thrown automatically during request body parsing. You do not need to handle these yourself — they are caught by the framework and converted to appropriate HTTP responses.
382
+
383
+ | Status | Condition |
384
+ |--------|-----------|
385
+ | **400** | Malformed JSON, invalid URL-encoded data, or corrupted multipart form data |
386
+ | **408** | Body parsing timed out (exceeds `body.timeout`, default 30 seconds) |
387
+ | **413** | Request body exceeds `body.max_size` (default 10 MB) |
388
+ | **415** | Unsupported content type (not JSON, URL-encoded, or multipart) |
389
+
390
+ These limits are configured via [Configuration](./configuration.md) (`body.max_size`, `body.timeout`).
391
+
392
+ Supported content types:
393
+ - `application/json`
394
+ - `application/x-www-form-urlencoded`
395
+ - `multipart/form-data`
396
+
397
+ ## Error Flow
398
+
399
+ When any error occurs in your handler or middleware, the framework uses the **same** `ammo.throw(err)` you use for intentional errors — one mechanism:
400
+
401
+ 1. The framework's `executeChain()` catches the error
402
+ 2. If `LOG_EXCEPTIONS` is enabled, the error is logged
403
+ 3. The error is passed to `ammo.throw(err)` (no separate "send response" step — `ammo.throw()` does it)
404
+ 4. **TejError** — uses the error's `code` and `message` directly
405
+ 5. **When errors.llm.enabled** — LLM infers status and message from code context (same as explicit `ammo.throw()`)
406
+ 6. **Otherwise** — 500 with the error message or string representation
407
+ 7. `ammo.throw()` sends the HTTP response via `ammo.fire(statusCode, message)`
408
+
409
+ Once a response has been sent (`res.headersSent` is true), no further middleware or handlers execute.
410
+
411
+ ## Error Codes Reference
412
+
413
+ | Status | Name | When to Use |
414
+ |--------|------|-------------|
415
+ | 400 | Bad Request | Invalid input, malformed request |
416
+ | 401 | Unauthorized | Missing or invalid authentication |
417
+ | 403 | Forbidden | Authenticated but not authorized |
418
+ | 404 | Not Found | Resource doesn't exist |
419
+ | 405 | Method Not Allowed | HTTP method not supported |
420
+ | 409 | Conflict | Resource conflict (duplicate) |
421
+ | 413 | Payload Too Large | Request body too large |
422
+ | 422 | Unprocessable Entity | Valid syntax but semantic errors |
423
+ | 429 | Too Many Requests | Rate limit exceeded |
424
+ | 500 | Internal Server Error | Unexpected server errors |
425
+ | 502 | Bad Gateway | Upstream server error |
426
+ | 503 | Service Unavailable | Server temporarily unavailable |
427
+ | 504 | Gateway Timeout | Upstream server timeout |
428
+
429
+ ## Best Practices
430
+
431
+ 1. **Use appropriate status codes** — Don't return 500 for client errors
432
+ 2. **Provide useful messages** — Help developers debug issues
433
+ 3. **Don't expose internals** — Hide stack traces in production
434
+ 4. **Log errors** — Enable exception logging for debugging
435
+ 5. **Be consistent** — Use the same error format throughout your API
436
+ 6. **Validate early** — Check input before processing
437
+ 7. **Use TejError or ammo.throw(code, message)** — For HTTP-specific errors when you want explicit control
438
+ 8. **Opt in to LLM-inferred errors when helpful** — Enable via `errors.llm.enabled` in config or **`app.withLLMErrors(config?)`** before `takeoff()`, then configure baseURL, apiKey, and model so you can throw without specifying code or message and let the LLM infer them; see [Configuration](./configuration.md#error-handling-llm-inferred-errors)