@wooksjs/event-http 0.6.2 → 0.6.4

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.
@@ -0,0 +1,253 @@
1
+ # Error Handling — @wooksjs/event-http
2
+
3
+ > Covers throwing HTTP errors, custom error bodies, error rendering, and error flow.
4
+
5
+ ## Concepts
6
+
7
+ In Wooks, errors are raised by throwing an `HttpError` instance. The framework catches it and renders an appropriate error response based on the client's `Accept` header (JSON, HTML, or plain text).
8
+
9
+ Any uncaught `Error` thrown in a handler is automatically wrapped as a 500 Internal Server Error.
10
+
11
+ ## `HttpError`
12
+
13
+ ```ts
14
+ import { HttpError } from '@wooksjs/event-http'
15
+ ```
16
+
17
+ ### Basic usage
18
+
19
+ ```ts
20
+ app.get('/users/:id', async () => {
21
+ const user = await db.findUser(id)
22
+ if (!user) {
23
+ throw new HttpError(404, 'User not found')
24
+ }
25
+ return user
26
+ })
27
+ ```
28
+
29
+ This produces a response like:
30
+
31
+ ```json
32
+ { "statusCode": 404, "error": "Not Found", "message": "User not found" }
33
+ ```
34
+
35
+ ### Status code only
36
+
37
+ ```ts
38
+ throw new HttpError(403)
39
+ // → { "statusCode": 403, "error": "Forbidden", "message": "" }
40
+ ```
41
+
42
+ The `error` field is automatically populated from the standard HTTP status text.
43
+
44
+ ### Custom error body
45
+
46
+ Pass an object as the second argument for additional fields:
47
+
48
+ ```ts
49
+ throw new HttpError(422, {
50
+ message: 'Validation failed',
51
+ statusCode: 422,
52
+ fields: {
53
+ email: 'Invalid email format',
54
+ age: 'Must be positive',
55
+ },
56
+ })
57
+ ```
58
+
59
+ Response:
60
+
61
+ ```json
62
+ {
63
+ "statusCode": 422,
64
+ "error": "Unprocessable Entity",
65
+ "message": "Validation failed",
66
+ "fields": {
67
+ "email": "Invalid email format",
68
+ "age": "Must be positive"
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### Constructor signature
74
+
75
+ ```ts
76
+ class HttpError<T extends TWooksErrorBody = TWooksErrorBody> extends Error {
77
+ constructor(
78
+ code: THttpErrorCodes = 500, // HTTP status code (4xx, 5xx)
79
+ body: string | T = '', // message string or structured body
80
+ )
81
+
82
+ get body(): TWooksErrorBodyExt // always returns { statusCode, message, error, ...extra }
83
+ }
84
+ ```
85
+
86
+ ### Error body shape
87
+
88
+ ```ts
89
+ interface TWooksErrorBody {
90
+ message: string
91
+ statusCode: EHttpStatusCode
92
+ error?: string
93
+ }
94
+
95
+ // Extended (always has error string)
96
+ interface TWooksErrorBodyExt extends TWooksErrorBody {
97
+ error: string // e.g. 'Not Found', 'Internal Server Error'
98
+ }
99
+ ```
100
+
101
+ ## Error Flow
102
+
103
+ 1. Handler throws `HttpError` → framework catches it
104
+ 2. Error body is constructed via `httpError.body` getter
105
+ 3. `HttpErrorRenderer` checks the `Accept` header:
106
+ - `application/json` → JSON response
107
+ - `text/html` → styled HTML error page
108
+ - `text/plain` → plain text
109
+ - fallback → JSON
110
+ 4. Status code from the error is set on the response
111
+
112
+ ### Uncaught errors
113
+
114
+ Any non-`HttpError` thrown in a handler is wrapped as a 500:
115
+
116
+ ```ts
117
+ app.get('/crash', () => {
118
+ throw new Error('something broke')
119
+ // → 500 Internal Server Error: "something broke"
120
+ })
121
+ ```
122
+
123
+ ### Error in handler chain
124
+
125
+ If multiple handlers are registered for a route, an error in a non-last handler falls through to the next handler. Only the last handler's error is sent as the response:
126
+
127
+ ```ts
128
+ // This is the internal behavior — typically you register one handler per route
129
+ ```
130
+
131
+ ## Common Patterns
132
+
133
+ ### Pattern: Guard function
134
+
135
+ ```ts
136
+ function requireAuth() {
137
+ const { isBearer, authRawCredentials } = useAuthorization()
138
+ if (!isBearer()) {
139
+ throw new HttpError(401, 'Authentication required')
140
+ }
141
+ const token = authRawCredentials()!
142
+ const user = verifyToken(token)
143
+ if (!user) {
144
+ throw new HttpError(401, 'Invalid token')
145
+ }
146
+ return user
147
+ }
148
+
149
+ app.get('/protected', () => {
150
+ const user = requireAuth()
151
+ return { message: `Hello ${user.name}` }
152
+ })
153
+ ```
154
+
155
+ ### Pattern: Validation with details
156
+
157
+ ```ts
158
+ function validateBody(data: unknown): asserts data is CreateUserDTO {
159
+ const errors: Record<string, string> = {}
160
+
161
+ if (!data || typeof data !== 'object') {
162
+ throw new HttpError(400, 'Request body must be an object')
163
+ }
164
+
165
+ const body = data as Record<string, unknown>
166
+ if (!body.email) errors.email = 'Required'
167
+ if (!body.name) errors.name = 'Required'
168
+
169
+ if (Object.keys(errors).length > 0) {
170
+ throw new HttpError(422, {
171
+ message: 'Validation failed',
172
+ statusCode: 422,
173
+ fields: errors,
174
+ })
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### Pattern: Not Found with context
180
+
181
+ ```ts
182
+ app.get('/users/:id', async () => {
183
+ const { get } = useRouteParams<{ id: string }>()
184
+ const id = get('id')
185
+ const user = await db.findUser(id)
186
+ if (!user) {
187
+ throw new HttpError(404, `User with id "${id}" not found`)
188
+ }
189
+ return user
190
+ })
191
+ ```
192
+
193
+ ### Pattern: Custom 404 handler
194
+
195
+ ```ts
196
+ const app = createHttpApp({
197
+ onNotFound: () => {
198
+ const { url } = useRequest()
199
+ throw new HttpError(404, `Route ${url} does not exist`)
200
+ },
201
+ })
202
+ ```
203
+
204
+ ## Available Status Codes
205
+
206
+ `HttpError` accepts any valid HTTP error status code. Common ones:
207
+
208
+ | Code | Meaning |
209
+ |------|---------|
210
+ | 400 | Bad Request |
211
+ | 401 | Unauthorized |
212
+ | 403 | Forbidden |
213
+ | 404 | Not Found |
214
+ | 405 | Method Not Allowed |
215
+ | 408 | Request Timeout |
216
+ | 409 | Conflict |
217
+ | 413 | Payload Too Large |
218
+ | 415 | Unsupported Media Type |
219
+ | 416 | Range Not Satisfiable |
220
+ | 422 | Unprocessable Entity |
221
+ | 429 | Too Many Requests |
222
+ | 500 | Internal Server Error |
223
+ | 502 | Bad Gateway |
224
+ | 503 | Service Unavailable |
225
+
226
+ The `EHttpStatusCode` enum from `@wooksjs/event-http` provides all standard codes.
227
+
228
+ ## Built-in Error Responses
229
+
230
+ The framework automatically throws `HttpError` in certain situations:
231
+
232
+ | Situation | Code | Message |
233
+ |-----------|------|---------|
234
+ | No route matched | 404 | (empty) |
235
+ | Body too large (compressed) | 413 | Payload Too Large |
236
+ | Body too large (inflated) | 413 | Inflated body too large |
237
+ | Compression ratio too high | 413 | Compression ratio too high |
238
+ | Unsupported Content-Encoding | 415 | Unsupported Content-Encoding "..." |
239
+ | Body read timeout | 408 | Request body timeout |
240
+ | Malformed JSON body | 400 | (parse error message) |
241
+ | Missing form-data boundary | 400 | form-data boundary not recognized |
242
+
243
+ ## Best Practices
244
+
245
+ - **Throw `HttpError`, don't return error objects** — The framework detects `HttpError` specially and renders it with correct status and content negotiation.
246
+ - **Use meaningful status codes** — 400 for bad input, 401 for missing auth, 403 for insufficient permissions, 404 for not found, 422 for validation errors.
247
+ - **Include context in error messages** — `"User with id 42 not found"` is more useful than `"Not found"`.
248
+ - **Use guard functions** — Extract auth/validation into reusable functions that throw on failure.
249
+
250
+ ## Gotchas
251
+
252
+ - Throwing a plain `Error` (not `HttpError`) results in a 500 with the error's message exposed. In production, you may want to catch and wrap errors to avoid leaking internals.
253
+ - `HttpError.body` always includes an `error` field with the standard HTTP status text, even if you didn't provide one.