@web-ts-toolkit/http-errors 0.31.5 → 0.33.0

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/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/status.ts
2
+ import assert from "assert";
2
3
  import { STATUS_CODES } from "http";
3
- var canonicalStatusByHttpStatus = {
4
+ var canonicalStatusLookup = Object.freeze({
4
5
  400: "INVALID_ARGUMENT",
5
6
  401: "UNAUTHENTICATED",
6
7
  403: "PERMISSION_DENIED",
@@ -12,26 +13,77 @@ var canonicalStatusByHttpStatus = {
12
13
  501: "UNIMPLEMENTED",
13
14
  503: "UNAVAILABLE",
14
15
  504: "DEADLINE_EXCEEDED"
15
- };
16
- var getCanonicalStatus = (statusCode) => canonicalStatusByHttpStatus[statusCode] || "UNKNOWN";
16
+ });
17
+ var canonicalStatusByHttpStatus = Object.freeze({
18
+ ...canonicalStatusLookup
19
+ });
20
+ var getCanonicalStatus = (statusCode) => canonicalStatusLookup[statusCode] || "UNKNOWN";
17
21
  var getStatusTitle = (statusCode) => STATUS_CODES[statusCode] || "Unknown Error";
22
+ var isHttpErrorStatusCode = (statusCode) => typeof statusCode === "number" && Number.isFinite(statusCode) && Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599;
23
+ var validateHttpErrorStatusCode = (statusCode, source = "statusCode") => {
24
+ assert.ok(
25
+ isHttpErrorStatusCode(statusCode),
26
+ `${source} must be an integer HTTP error status code between 400 and 599`
27
+ );
28
+ return statusCode;
29
+ };
30
+ var validateClientErrorStatusCode = (statusCode, source = "statusCode") => {
31
+ validateHttpErrorStatusCode(statusCode, source);
32
+ assert.ok(typeof statusCode === "number" && statusCode <= 499, `${source} must be a 4xx HTTP error status code`);
33
+ return statusCode;
34
+ };
35
+ var validateServerErrorStatusCode = (statusCode, source = "statusCode") => {
36
+ validateHttpErrorStatusCode(statusCode, source);
37
+ assert.ok(typeof statusCode === "number" && statusCode >= 500, `${source} must be a 5xx HTTP error status code`);
38
+ return statusCode;
39
+ };
18
40
 
19
41
  // src/serialize.ts
20
42
  var RFC_9457_DEFAULT_TYPE = "about:blank";
43
+ var RFC_9457_VALIDATION_ERROR_OPTIONAL_FIELDS = ["pointer", "parameter", "header"];
21
44
  var toAip193ErrorDetails = (error, fallbackDomain) => {
22
45
  const status = error.status ?? getCanonicalStatus(error.statusCode);
23
46
  return [
24
47
  createAip193ErrorInfoDetail(error.reason ?? status, error.domain ?? fallbackDomain, error.metadata),
25
- ...error.errors !== void 0 ? [{ type: "bad_request", errors: error.errors }] : [],
48
+ ...error.errors !== void 0 ? [{ type: "bad_request", errors: Array.isArray(error.errors) ? [...error.errors] : error.errors }] : [],
26
49
  ...error.details ?? []
27
50
  ];
28
51
  };
29
- var toRfc9457Errors = (errors) => Array.isArray(errors) ? errors : void 0;
52
+ var toRfc9457Errors = (errors) => Array.isArray(errors) && errors.length > 0 ? [...errors] : void 0;
53
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
54
+ var toRfc9457ValidationError = (value) => {
55
+ if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, "detail") || typeof value.detail !== "string") {
56
+ return void 0;
57
+ }
58
+ const validationError = {
59
+ detail: value.detail
60
+ };
61
+ for (const field of RFC_9457_VALIDATION_ERROR_OPTIONAL_FIELDS) {
62
+ if (Object.prototype.hasOwnProperty.call(value, field)) {
63
+ const fieldValue = value[field];
64
+ if (typeof fieldValue !== "string") {
65
+ return void 0;
66
+ }
67
+ validationError[field] = fieldValue;
68
+ }
69
+ }
70
+ return validationError;
71
+ };
72
+ var toRfc9457ValidationErrors = (errors) => {
73
+ if (!Array.isArray(errors)) {
74
+ return void 0;
75
+ }
76
+ const validationErrors = errors.flatMap((entry) => {
77
+ const validationError = toRfc9457ValidationError(entry);
78
+ return validationError ? [validationError] : [];
79
+ });
80
+ return validationErrors.length > 0 ? validationErrors : void 0;
81
+ };
30
82
  var createAip193ErrorInfoDetail = (reason, domain, metadata) => ({
31
83
  type: "error_info",
32
84
  reason,
33
85
  domain,
34
- ...metadata ? { metadata } : {}
86
+ ...metadata ? { metadata: { ...metadata } } : {}
35
87
  });
36
88
  var toAip193ErrorPayload = (error, fallbackDomain = "http-errors") => {
37
89
  const status = error.status ?? getCanonicalStatus(error.statusCode);
@@ -45,7 +97,7 @@ var toAip193ErrorPayload = (error, fallbackDomain = "http-errors") => {
45
97
  }
46
98
  };
47
99
  };
48
- var toRfc9457ErrorPayload = (error) => {
100
+ function toRfc9457ErrorPayload(error) {
49
101
  const errors = toRfc9457Errors(error.errors);
50
102
  return {
51
103
  type: error.type ?? RFC_9457_DEFAULT_TYPE,
@@ -55,67 +107,237 @@ var toRfc9457ErrorPayload = (error) => {
55
107
  ...error.instance ? { instance: error.instance } : {},
56
108
  ...errors ? { errors } : {}
57
109
  };
110
+ }
111
+ var toRfc9457ValidationErrorPayload = (error) => {
112
+ const errors = toRfc9457ValidationErrors(error.errors);
113
+ return {
114
+ type: error.type ?? RFC_9457_DEFAULT_TYPE,
115
+ title: error.title ?? getStatusTitle(error.statusCode),
116
+ status: error.statusCode,
117
+ detail: error.message,
118
+ ...error.instance ? { instance: error.instance } : {},
119
+ ...errors ? { errors } : {}
120
+ };
58
121
  };
59
- var toRfc9457ValidationErrorPayload = (error) => toRfc9457ErrorPayload(error);
60
122
 
61
123
  // src/base.ts
62
124
  import { toStringRecord } from "@web-ts-toolkit/utils";
63
125
 
64
- // src/messages.ts
65
- var messages = {
66
- 400: "The server cannot process the request due to a client error",
67
- 401: "The user is not authorized",
68
- 403: "The server refused to authorize the request",
69
- 404: "The server did not find a current representation for the target resource",
70
- 405: "The method received is not allowed",
71
- 406: "The request is not acceptable to the user agent",
72
- 407: "The client needs to authenticate itself in order to use a proxy",
73
- 408: "The request was not completed in the expected time",
74
- 409: "The request was not completed due to a conflict with the target resource",
75
- 410: "The target resource is no longer available at the origin server",
76
- 411: "The server refuses to accept the request without a defined Content-Length",
77
- 412: "One or more conditions given in the request header fields evaluated to false",
78
- 413: "The request payload is too large",
79
- 414: "The request target is too long",
80
- 415: "The payload is in a format not supported",
81
- 416: "None of the ranges in the request's Range header field overlap the current extent of the selected resource",
82
- 417: "The expectation given in the request's Expect header field could not be met",
83
- 418: "I'm a teapot",
84
- 421: "The request was directed at a server that is not able to produce a response",
85
- 422: "The server is unable to process the request",
86
- 423: "The source or destination resource of a method is locked",
87
- 424: "The requested action depended on another action",
88
- 426: "This service requires use of a different protocol",
89
- 428: "This request is required to be conditional",
90
- 429: "The user has sent too many requests in a given amount of time",
91
- 431: "Request header fields too large",
92
- 451: "Denied access due to a consequence of a legal demand",
93
- 500: "The server encountered an unexpected condition",
94
- 501: "The server does not support the functionality required to fulfill the request",
95
- 502: "The server received an invalid response from an upstream server",
96
- 503: "The server is temporarily unable to handle the request",
97
- 504: "The server did not receive a timely response from an upstream server",
98
- 505: "The server does not support the HTTP protocol version used in the request",
99
- 506: "The server has an internal configuration error",
100
- 507: "The server is unable to store the representation needed to complete the request",
101
- 508: "The server detected an infinite loop while processing the request",
102
- 510: "Further extensions to the request are required for the server to fulfill it",
103
- 511: "The client needs to authenticate to gain network access"
126
+ // src/status-definitions.ts
127
+ var httpErrorStatusDefinitions = {
128
+ BadRequestError: {
129
+ statusCode: 400,
130
+ category: "client",
131
+ message: "The server cannot process the request due to a client error"
132
+ },
133
+ UnauthorizedError: {
134
+ statusCode: 401,
135
+ category: "client",
136
+ message: "The user is not authorized"
137
+ },
138
+ ForbiddenError: {
139
+ statusCode: 403,
140
+ category: "client",
141
+ message: "The server refused to authorize the request"
142
+ },
143
+ NotFoundError: {
144
+ statusCode: 404,
145
+ category: "client",
146
+ message: "The server did not find a current representation for the target resource"
147
+ },
148
+ MethodNotAllowedError: {
149
+ statusCode: 405,
150
+ category: "client",
151
+ message: "The method received is not allowed"
152
+ },
153
+ NotAcceptableError: {
154
+ statusCode: 406,
155
+ category: "client",
156
+ message: "The request is not acceptable to the user agent"
157
+ },
158
+ ProxyAuthRequiredError: {
159
+ statusCode: 407,
160
+ category: "client",
161
+ message: "The client needs to authenticate itself in order to use a proxy"
162
+ },
163
+ RequestTimeoutError: {
164
+ statusCode: 408,
165
+ category: "client",
166
+ message: "The request was not completed in the expected time"
167
+ },
168
+ ConflictError: {
169
+ statusCode: 409,
170
+ category: "client",
171
+ message: "The request was not completed due to a conflict with the target resource"
172
+ },
173
+ GoneError: {
174
+ statusCode: 410,
175
+ category: "client",
176
+ message: "The target resource is no longer available at the origin server"
177
+ },
178
+ LengthRequiredError: {
179
+ statusCode: 411,
180
+ category: "client",
181
+ message: "The server refuses to accept the request without a defined Content-Length"
182
+ },
183
+ PreconditionFailedError: {
184
+ statusCode: 412,
185
+ category: "client",
186
+ message: "One or more conditions given in the request header fields evaluated to false"
187
+ },
188
+ PayloadTooLargeError: {
189
+ statusCode: 413,
190
+ category: "client",
191
+ message: "The request payload is too large"
192
+ },
193
+ UriTooLongError: {
194
+ statusCode: 414,
195
+ category: "client",
196
+ message: "The request target is too long"
197
+ },
198
+ UnsupportedMediaTypeError: {
199
+ statusCode: 415,
200
+ category: "client",
201
+ message: "The payload is in a format not supported"
202
+ },
203
+ RequestedRangeNotSatisfiableError: {
204
+ statusCode: 416,
205
+ category: "client",
206
+ message: "None of the ranges in the request's Range header field overlap the current extent of the selected resource"
207
+ },
208
+ ExpectationFailedError: {
209
+ statusCode: 417,
210
+ category: "client",
211
+ message: "The expectation given in the request's Expect header field could not be met"
212
+ },
213
+ TeapotError: {
214
+ statusCode: 418,
215
+ category: "client",
216
+ message: "I'm a teapot"
217
+ },
218
+ MisdirectedRequestError: {
219
+ statusCode: 421,
220
+ category: "client",
221
+ message: "The request was directed at a server that is not able to produce a response"
222
+ },
223
+ UnprocessableEntityError: {
224
+ statusCode: 422,
225
+ category: "client",
226
+ message: "The server is unable to process the request"
227
+ },
228
+ LockedError: {
229
+ statusCode: 423,
230
+ category: "client",
231
+ message: "The source or destination resource of a method is locked"
232
+ },
233
+ FailedDependencyError: {
234
+ statusCode: 424,
235
+ category: "client",
236
+ message: "The requested action depended on another action"
237
+ },
238
+ UpgradeRequiredError: {
239
+ statusCode: 426,
240
+ category: "client",
241
+ message: "This service requires use of a different protocol"
242
+ },
243
+ PreconditionRequiredError: {
244
+ statusCode: 428,
245
+ category: "client",
246
+ message: "This request is required to be conditional"
247
+ },
248
+ TooManyRequestsError: {
249
+ statusCode: 429,
250
+ category: "client",
251
+ message: "The user has sent too many requests in a given amount of time"
252
+ },
253
+ RequestHeaderFieldsTooLargeError: {
254
+ statusCode: 431,
255
+ category: "client",
256
+ message: "Request header fields too large"
257
+ },
258
+ UnavailableForLegalReasonsError: {
259
+ statusCode: 451,
260
+ category: "client",
261
+ message: "Denied access due to a consequence of a legal demand"
262
+ },
263
+ InternalServerError: {
264
+ statusCode: 500,
265
+ category: "server",
266
+ message: "The server encountered an unexpected condition"
267
+ },
268
+ NotImplementedError: {
269
+ statusCode: 501,
270
+ category: "server",
271
+ message: "The server does not support the functionality required to fulfill the request"
272
+ },
273
+ BadGatewayError: {
274
+ statusCode: 502,
275
+ category: "server",
276
+ message: "The server received an invalid response from an upstream server"
277
+ },
278
+ ServiceUnavailableError: {
279
+ statusCode: 503,
280
+ category: "server",
281
+ message: "The server is temporarily unable to handle the request"
282
+ },
283
+ GatewayTimeoutError: {
284
+ statusCode: 504,
285
+ category: "server",
286
+ message: "The server did not receive a timely response from an upstream server"
287
+ },
288
+ HttpVersionNotSupportedError: {
289
+ statusCode: 505,
290
+ category: "server",
291
+ message: "The server does not support the HTTP protocol version used in the request"
292
+ },
293
+ VariantAlsoNegotiatesError: {
294
+ statusCode: 506,
295
+ category: "server",
296
+ message: "The server has an internal configuration error"
297
+ },
298
+ InsufficientStorageError: {
299
+ statusCode: 507,
300
+ category: "server",
301
+ message: "The server is unable to store the representation needed to complete the request"
302
+ },
303
+ LoopDetectedError: {
304
+ statusCode: 508,
305
+ category: "server",
306
+ message: "The server detected an infinite loop while processing the request"
307
+ },
308
+ NotExtendedError: {
309
+ statusCode: 510,
310
+ category: "server",
311
+ message: "Further extensions to the request are required for the server to fulfill it"
312
+ },
313
+ NetworkAuthenticationRequiredError: {
314
+ statusCode: 511,
315
+ category: "server",
316
+ message: "The client needs to authenticate to gain network access"
317
+ }
104
318
  };
319
+
320
+ // src/messages.ts
321
+ var messages = Object.freeze(
322
+ Object.fromEntries(
323
+ Object.values(httpErrorStatusDefinitions).map(({ statusCode, message }) => [statusCode, message])
324
+ )
325
+ );
105
326
  var getDefaultMessage = (statusCode) => messages[statusCode] || messages[500];
106
327
 
107
328
  // src/base.ts
108
329
  var ErrorCtor = Error;
109
330
  var HttpError = class extends Error {
110
331
  constructor(statusCode = 500, message, options = {}) {
111
- super(message ?? getDefaultMessage(statusCode), options);
332
+ const validatedStatusCode = validateHttpErrorStatusCode(statusCode);
333
+ super(message ?? getDefaultMessage(validatedStatusCode), options);
112
334
  const { status, reason, domain, metadata, details, errors, type, title, instance } = options;
113
335
  if (ErrorCtor.captureStackTrace) {
114
336
  ErrorCtor.captureStackTrace(this, this.constructor);
115
337
  }
116
338
  this.name = this.constructor.name;
117
- this.statusCode = statusCode;
118
- this.status = status ?? getCanonicalStatus(statusCode);
339
+ this.statusCode = validatedStatusCode;
340
+ this.status = status ?? getCanonicalStatus(validatedStatusCode);
119
341
  this.date = /* @__PURE__ */ new Date();
120
342
  if (reason !== void 0) {
121
343
  this.reason = reason;
@@ -125,13 +347,13 @@ var HttpError = class extends Error {
125
347
  }
126
348
  const normalizedMetadata = toStringRecord(metadata);
127
349
  if (normalizedMetadata !== void 0) {
128
- this.metadata = normalizedMetadata;
350
+ this.metadata = Object.freeze(normalizedMetadata);
129
351
  }
130
352
  if (details !== void 0) {
131
- this.details = details;
353
+ this.details = Object.freeze([...details]);
132
354
  }
133
355
  if (errors !== void 0) {
134
- this.errors = errors;
356
+ this.errors = Array.isArray(errors) ? Object.freeze([...errors]) : errors;
135
357
  }
136
358
  if (type !== void 0) {
137
359
  this.type = type;
@@ -146,12 +368,12 @@ var HttpError = class extends Error {
146
368
  };
147
369
  var ClientError = class extends HttpError {
148
370
  constructor(statusCode = 400, message, options = {}) {
149
- super(statusCode, message, options);
371
+ super(validateClientErrorStatusCode(statusCode), message, options);
150
372
  }
151
373
  };
152
374
  var ServerError = class extends HttpError {
153
375
  constructor(statusCode = 500, message, options = {}) {
154
- super(statusCode, message, options);
376
+ super(validateServerErrorStatusCode(statusCode), message, options);
155
377
  }
156
378
  };
157
379
 
@@ -394,7 +616,11 @@ export {
394
616
  createAip193ErrorInfoDetail,
395
617
  getCanonicalStatus,
396
618
  getStatusTitle,
619
+ isHttpErrorStatusCode,
397
620
  toAip193ErrorPayload,
398
621
  toRfc9457ErrorPayload,
399
- toRfc9457ValidationErrorPayload
622
+ toRfc9457ValidationErrorPayload,
623
+ validateClientErrorStatusCode,
624
+ validateHttpErrorStatusCode,
625
+ validateServerErrorStatusCode
400
626
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/http-errors",
3
3
  "description": "Typed HTTP error classes and payload helpers for backend APIs",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/http-errors",
5
- "version": "0.31.5",
5
+ "version": "0.33.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "http-errors",
@@ -16,17 +16,21 @@
16
16
  "types": "./index.d.ts",
17
17
  "exports": {
18
18
  ".": {
19
- "types": "./index.d.ts",
19
+ "types": {
20
+ "import": "./index.d.mts",
21
+ "require": "./index.d.ts",
22
+ "default": "./index.d.ts"
23
+ },
20
24
  "import": "./index.mjs",
21
25
  "require": "./index.js",
22
26
  "default": "./index.js"
23
27
  }
24
28
  },
25
29
  "engines": {
26
- "node": ">=20"
30
+ "node": ">=22"
27
31
  },
28
32
  "dependencies": {
29
- "@web-ts-toolkit/utils": "0.31.5"
33
+ "@web-ts-toolkit/utils": "0.33.0"
30
34
  },
31
35
  "files": [
32
36
  "**/*",