@stacksjs/http 0.70.53 → 0.70.55

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 (2) hide show
  1. package/package.json +3 -2
  2. package/src/index.ts +123 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/http",
3
3
  "type": "module",
4
- "version": "0.70.53",
4
+ "version": "0.70.55",
5
5
  "description": "Stacks HTTP methods.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -40,7 +40,8 @@
40
40
  "types": "dist/index.d.ts",
41
41
  "files": [
42
42
  "README.md",
43
- "dist"
43
+ "dist",
44
+ "src"
44
45
  ],
45
46
  "scripts": {
46
47
  "build": "bun build.ts",
package/src/index.ts ADDED
@@ -0,0 +1,123 @@
1
+ export enum Response {
2
+ // 1xx Informational
3
+ HTTP_CONTINUE = 100,
4
+ HTTP_SWITCHING_PROTOCOLS = 101,
5
+
6
+ // 2xx Success
7
+ HTTP_OK = 200,
8
+ HTTP_CREATED = 201,
9
+ HTTP_ACCEPTED = 202,
10
+ HTTP_NON_AUTHORITATIVE_INFORMATION = 203,
11
+ HTTP_NO_CONTENT = 204,
12
+ HTTP_RESET_CONTENT = 205,
13
+ HTTP_PARTIAL_CONTENT = 206,
14
+
15
+ // 3xx Redirection
16
+ HTTP_MULTIPLE_CHOICES = 300,
17
+ HTTP_MOVED_PERMANENTLY = 301,
18
+ HTTP_FOUND = 302,
19
+ HTTP_SEE_OTHER = 303,
20
+ HTTP_NOT_MODIFIED = 304,
21
+ HTTP_USE_PROXY = 305,
22
+ HTTP_UNUSED = 306,
23
+ HTTP_TEMPORARY_REDIRECT = 307,
24
+ HTTP_PERMANENT_REDIRECT = 308,
25
+
26
+ // 4xx Client Error
27
+ HTTP_BAD_REQUEST = 400,
28
+ HTTP_UNAUTHORIZED = 401,
29
+ HTTP_PAYMENT_REQUIRED = 402,
30
+ HTTP_FORBIDDEN = 403,
31
+ HTTP_NOT_FOUND = 404,
32
+ HTTP_METHOD_NOT_ALLOWED = 405,
33
+ HTTP_NOT_ACCEPTABLE = 406,
34
+ HTTP_PROXY_AUTHENTICATION_REQUIRED = 407,
35
+ HTTP_REQUEST_TIMEOUT = 408,
36
+ HTTP_CONFLICT = 409,
37
+ HTTP_GONE = 410,
38
+ HTTP_LENGTH_REQUIRED = 411,
39
+ HTTP_PRECONDITION_FAILED = 412,
40
+ HTTP_REQUEST_ENTITY_TOO_LARGE = 413,
41
+ HTTP_REQUEST_URI_TOO_LONG = 414,
42
+ HTTP_UNSUPPORTED_MEDIA_TYPE = 415,
43
+ HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416,
44
+ HTTP_EXPECTATION_FAILED = 417,
45
+ HTTP_I_AM_A_TEAPOT = 418,
46
+ HTTP_MISDIRECTED_REQUEST = 421,
47
+ HTTP_UNPROCESSABLE_ENTITY = 422,
48
+ HTTP_LOCKED = 423,
49
+ HTTP_FAILED_DEPENDENCY = 424,
50
+ HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425,
51
+ HTTP_UPGRADE_REQUIRED = 426,
52
+ HTTP_PRECONDITION_REQUIRED = 428,
53
+ HTTP_TOO_MANY_REQUESTS = 429,
54
+ HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
55
+ HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451,
56
+
57
+ // 5xx Server Error
58
+ HTTP_INTERNAL_SERVER_ERROR = 500,
59
+ HTTP_NOT_IMPLEMENTED = 501,
60
+ HTTP_BAD_GATEWAY = 502,
61
+ HTTP_SERVICE_UNAVAILABLE = 503,
62
+ HTTP_GATEWAY_TIMEOUT = 504,
63
+ HTTP_VERSION_NOT_SUPPORTED = 505,
64
+ HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506,
65
+ HTTP_INSUFFICIENT_STORAGE = 507,
66
+ HTTP_LOOP_DETECTED = 508,
67
+ HTTP_NOT_EXTENDED = 510,
68
+ HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511,
69
+ }
70
+
71
+ /**
72
+ * `fetch` wrapper that aborts after `timeoutMs` and honors `Retry-After`
73
+ * on 429/503 responses with exponential backoff.
74
+ *
75
+ * Platform `fetch()` has no timeout — a misbehaving upstream that accepts
76
+ * the connection but never sends bytes leaves the caller hanging forever.
77
+ * Worse, every driver (Stripe webhooks, Twilio, AI providers) shares the
78
+ * same code path, so one slow upstream cascades into stalled queue
79
+ * workers. This wrapper keeps the `fetch(input, init)` API but adds a
80
+ * budget so failure modes stay bounded.
81
+ *
82
+ * @param input URL or Request, exactly like fetch
83
+ * @param init RequestInit + { timeoutMs?: number, retry?: number }
84
+ * - timeoutMs (default 30_000) — overall request budget
85
+ * - retry (default 0) — retry count on 429/503
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const res = await fetchWithBudget('https://api.example.com', {
90
+ * method: 'POST',
91
+ * body: JSON.stringify(payload),
92
+ * timeoutMs: 5000,
93
+ * retry: 3,
94
+ * })
95
+ * ```
96
+ */
97
+ export async function fetchWithBudget(
98
+ input: RequestInfo | URL,
99
+ init: RequestInit & { timeoutMs?: number, retry?: number } = {},
100
+ ): Promise<globalThis.Response> {
101
+ const { timeoutMs = 30_000, retry = 0, ...rest } = init
102
+ let attempt = 0
103
+
104
+ // eslint-disable-next-line no-constant-condition
105
+ while (true) {
106
+ const ac = new AbortController()
107
+ const timeoutHandle = setTimeout(() => ac.abort(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs)
108
+ try {
109
+ const response = await fetch(input, { ...rest, signal: ac.signal })
110
+ if ((response.status === 429 || response.status === 503) && attempt < retry) {
111
+ const ra = Number(response.headers.get('retry-after'))
112
+ const delay = (Number.isFinite(ra) && ra > 0 ? ra : 2 ** attempt) * 1000
113
+ await new Promise(r => setTimeout(r, Math.min(delay, 30_000)))
114
+ attempt++
115
+ continue
116
+ }
117
+ return response
118
+ }
119
+ finally {
120
+ clearTimeout(timeoutHandle)
121
+ }
122
+ }
123
+ }