@sdxc/http 0.0.0-pre.1
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/LICENSE.md +21 -0
- package/README.md +549 -0
- package/dist/cache/conditional.d.ts +24 -0
- package/dist/cache/conditional.js +108 -0
- package/dist/cache/etag.d.ts +37 -0
- package/dist/cache/etag.js +33 -0
- package/dist/cache/http-date.d.ts +38 -0
- package/dist/cache/http-date.js +56 -0
- package/dist/cache/if-modified-since.d.ts +36 -0
- package/dist/cache/if-modified-since.js +53 -0
- package/dist/cache/index.d.ts +20 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/policies.d.ts +73 -0
- package/dist/cache/policies.js +78 -0
- package/dist/cache/policy.d.ts +63 -0
- package/dist/cache/policy.js +54 -0
- package/dist/cache/precondition.d.ts +53 -0
- package/dist/cache/precondition.js +53 -0
- package/dist/cache/vary.d.ts +24 -0
- package/dist/cache/vary.js +33 -0
- package/dist/content-type.d.ts +312 -0
- package/dist/content-type.js +312 -0
- package/dist/middleware/head-requests.d.ts +19 -0
- package/dist/middleware/head-requests.js +30 -0
- package/dist/negotiate.d.ts +97 -0
- package/dist/negotiate.js +156 -0
- package/dist/response/html.d.ts +272 -0
- package/dist/response/html.js +316 -0
- package/dist/response/json.d.ts +272 -0
- package/dist/response/json.js +316 -0
- package/dist/response.d.ts +171 -0
- package/dist/response.js +225 -0
- package/dist/status-code.d.ts +620 -0
- package/dist/status-code.js +486 -0
- package/package.json +33 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIME type constants for HTTP `Content-Type` header values.
|
|
3
|
+
*
|
|
4
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
5
|
+
* @copyright Sergio Xalambrí 2026
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Used for simple text responses without any formatting.
|
|
9
|
+
* @example
|
|
10
|
+
* headers.set("Content-Type", Text);
|
|
11
|
+
* @example
|
|
12
|
+
* return new Response("Hello", { headers: { "Content-Type": Text } });
|
|
13
|
+
*/
|
|
14
|
+
export const Text = "text/plain";
|
|
15
|
+
/**
|
|
16
|
+
* Used for web pages and HTML document responses.
|
|
17
|
+
* @example
|
|
18
|
+
* headers.set("Content-Type", HTML);
|
|
19
|
+
* @example
|
|
20
|
+
* return new Response("<h1>Hello</h1>", { headers: { "Content-Type": HTML } });
|
|
21
|
+
*/
|
|
22
|
+
export const HTML = "text/html";
|
|
23
|
+
/**
|
|
24
|
+
* Used for stylesheet responses.
|
|
25
|
+
* @example
|
|
26
|
+
* headers.set("Content-Type", CSS);
|
|
27
|
+
* @example
|
|
28
|
+
* return new Response("body { color: red; }", { headers: { "Content-Type": CSS } });
|
|
29
|
+
*/
|
|
30
|
+
export const CSS = "text/css";
|
|
31
|
+
/**
|
|
32
|
+
* Used for JavaScript file responses.
|
|
33
|
+
* @example
|
|
34
|
+
* headers.set("Content-Type", JavaScript);
|
|
35
|
+
* @example
|
|
36
|
+
* return new Response("console.log('hi')", { headers: { "Content-Type": JavaScript } });
|
|
37
|
+
*/
|
|
38
|
+
export const JavaScript = "text/javascript";
|
|
39
|
+
/**
|
|
40
|
+
* Used for comma-separated values data exports.
|
|
41
|
+
* @example
|
|
42
|
+
* headers.set("Content-Type", CSV);
|
|
43
|
+
* @example
|
|
44
|
+
* return new Response("name,age\nJohn,30", { headers: { "Content-Type": CSV } });
|
|
45
|
+
*/
|
|
46
|
+
export const CSV = "text/csv";
|
|
47
|
+
/**
|
|
48
|
+
* Used for XML documents intended for human readability.
|
|
49
|
+
* @example
|
|
50
|
+
* headers.set("Content-Type", XML);
|
|
51
|
+
* @example
|
|
52
|
+
* return new Response("<root><item/></root>", { headers: { "Content-Type": XML } });
|
|
53
|
+
*/
|
|
54
|
+
export const XML = "text/xml";
|
|
55
|
+
/**
|
|
56
|
+
* Used for Markdown-formatted text responses.
|
|
57
|
+
* @example
|
|
58
|
+
* headers.set("Content-Type", Markdown);
|
|
59
|
+
* @example
|
|
60
|
+
* return new Response("# Hello", { headers: { "Content-Type": Markdown } });
|
|
61
|
+
*/
|
|
62
|
+
export const Markdown = "text/markdown";
|
|
63
|
+
/**
|
|
64
|
+
* Used for API responses and requests with structured data.
|
|
65
|
+
* Named `Json` (not `JSON`) to avoid shadowing the global `JSON` object.
|
|
66
|
+
* @example
|
|
67
|
+
* headers.set("Content-Type", Json);
|
|
68
|
+
* @example
|
|
69
|
+
* if (contentType === Json) { parseJson(body); }
|
|
70
|
+
*/
|
|
71
|
+
export const Json = "application/json";
|
|
72
|
+
/**
|
|
73
|
+
* Used for newline-delimited JSON streaming responses.
|
|
74
|
+
* @example
|
|
75
|
+
* headers.set("Content-Type", JSONLines);
|
|
76
|
+
* @example
|
|
77
|
+
* return new Response('{"a":1}\n{"b":2}', { headers: { "Content-Type": JSONLines } });
|
|
78
|
+
*/
|
|
79
|
+
export const JSONLines = "application/jsonl";
|
|
80
|
+
/**
|
|
81
|
+
* PDF content type.
|
|
82
|
+
* Used for PDF document responses.
|
|
83
|
+
* @example
|
|
84
|
+
* headers.set("Content-Type", PDF);
|
|
85
|
+
* @example
|
|
86
|
+
* return new Response(pdfBuffer, { headers: { "Content-Type": PDF } });
|
|
87
|
+
*/
|
|
88
|
+
export const PDF = "application/pdf";
|
|
89
|
+
/**
|
|
90
|
+
* ZIP archive content type.
|
|
91
|
+
* Used for compressed archive file responses.
|
|
92
|
+
* @example
|
|
93
|
+
* headers.set("Content-Type", ZIP);
|
|
94
|
+
* @example
|
|
95
|
+
* return new Response(zipBuffer, { headers: { "Content-Type": ZIP } });
|
|
96
|
+
*/
|
|
97
|
+
export const ZIP = "application/zip";
|
|
98
|
+
/**
|
|
99
|
+
* GZip compressed content type.
|
|
100
|
+
* Used for gzip-compressed file responses.
|
|
101
|
+
* @example
|
|
102
|
+
* headers.set("Content-Type", GZip);
|
|
103
|
+
* @example
|
|
104
|
+
* return new Response(gzipBuffer, { headers: { "Content-Type": GZip } });
|
|
105
|
+
*/
|
|
106
|
+
export const GZip = "application/gzip";
|
|
107
|
+
/**
|
|
108
|
+
* Multipart form data content type.
|
|
109
|
+
* Used for form submissions with file uploads.
|
|
110
|
+
* @example
|
|
111
|
+
* if (contentType.startsWith(FormData)) { parseFormData(body); }
|
|
112
|
+
* @example
|
|
113
|
+
* headers.set("Content-Type", FormData);
|
|
114
|
+
*/
|
|
115
|
+
export const FormData = "multipart/form-data";
|
|
116
|
+
/**
|
|
117
|
+
* URL-encoded form content type.
|
|
118
|
+
* Used for simple form submissions without files.
|
|
119
|
+
* @example
|
|
120
|
+
* headers.set("Content-Type", FormURLEncoded);
|
|
121
|
+
* @example
|
|
122
|
+
* if (contentType === FormURLEncoded) { parseURLEncoded(body); }
|
|
123
|
+
*/
|
|
124
|
+
export const FormURLEncoded = "application/x-www-form-urlencoded";
|
|
125
|
+
/**
|
|
126
|
+
* Binary octet stream content type.
|
|
127
|
+
* Used for arbitrary binary data when type is unknown.
|
|
128
|
+
* @example
|
|
129
|
+
* headers.set("Content-Type", OctetStream);
|
|
130
|
+
* @example
|
|
131
|
+
* return new Response(binaryData, { headers: { "Content-Type": OctetStream } });
|
|
132
|
+
*/
|
|
133
|
+
export const OctetStream = "application/octet-stream";
|
|
134
|
+
/**
|
|
135
|
+
* Used for XML data in API responses and machine processing.
|
|
136
|
+
* @example
|
|
137
|
+
* headers.set("Content-Type", ApplicationXML);
|
|
138
|
+
* @example
|
|
139
|
+
* return new Response(xmlData, { headers: { "Content-Type": ApplicationXML } });
|
|
140
|
+
*/
|
|
141
|
+
export const ApplicationXML = "application/xml";
|
|
142
|
+
/**
|
|
143
|
+
* PNG image content type.
|
|
144
|
+
* Used for lossless raster image responses.
|
|
145
|
+
* @example
|
|
146
|
+
* headers.set("Content-Type", PNG);
|
|
147
|
+
* @example
|
|
148
|
+
* return new Response(pngBuffer, { headers: { "Content-Type": PNG } });
|
|
149
|
+
*/
|
|
150
|
+
export const PNG = "image/png";
|
|
151
|
+
/**
|
|
152
|
+
* JPEG image content type.
|
|
153
|
+
* Used for lossy compressed photo responses.
|
|
154
|
+
* @example
|
|
155
|
+
* headers.set("Content-Type", JPEG);
|
|
156
|
+
* @example
|
|
157
|
+
* return new Response(jpegBuffer, { headers: { "Content-Type": JPEG } });
|
|
158
|
+
*/
|
|
159
|
+
export const JPEG = "image/jpeg";
|
|
160
|
+
/**
|
|
161
|
+
* GIF image content type.
|
|
162
|
+
* Used for animated or simple graphics responses.
|
|
163
|
+
* @example
|
|
164
|
+
* headers.set("Content-Type", GIF);
|
|
165
|
+
* @example
|
|
166
|
+
* return new Response(gifBuffer, { headers: { "Content-Type": GIF } });
|
|
167
|
+
*/
|
|
168
|
+
export const GIF = "image/gif";
|
|
169
|
+
/**
|
|
170
|
+
* WebP image content type.
|
|
171
|
+
* Used for modern lossy/lossless image responses.
|
|
172
|
+
* @example
|
|
173
|
+
* headers.set("Content-Type", WebP);
|
|
174
|
+
* @example
|
|
175
|
+
* return new Response(webpBuffer, { headers: { "Content-Type": WebP } });
|
|
176
|
+
*/
|
|
177
|
+
export const WebP = "image/webp";
|
|
178
|
+
/**
|
|
179
|
+
* SVG image content type.
|
|
180
|
+
* Used for vector graphics responses.
|
|
181
|
+
* @example
|
|
182
|
+
* headers.set("Content-Type", SVG);
|
|
183
|
+
* @example
|
|
184
|
+
* return new Response(svgString, { headers: { "Content-Type": SVG } });
|
|
185
|
+
*/
|
|
186
|
+
export const SVG = "image/svg+xml";
|
|
187
|
+
/**
|
|
188
|
+
* ICO image content type.
|
|
189
|
+
* Used for favicon and icon file responses.
|
|
190
|
+
* @example
|
|
191
|
+
* headers.set("Content-Type", ICO);
|
|
192
|
+
* @example
|
|
193
|
+
* return new Response(icoBuffer, { headers: { "Content-Type": ICO } });
|
|
194
|
+
*/
|
|
195
|
+
export const ICO = "image/x-icon";
|
|
196
|
+
/**
|
|
197
|
+
* AVIF image content type.
|
|
198
|
+
* Used for highly compressed modern image responses.
|
|
199
|
+
* @example
|
|
200
|
+
* headers.set("Content-Type", AVIF);
|
|
201
|
+
* @example
|
|
202
|
+
* return new Response(avifBuffer, { headers: { "Content-Type": AVIF } });
|
|
203
|
+
*/
|
|
204
|
+
export const AVIF = "image/avif";
|
|
205
|
+
/**
|
|
206
|
+
* MP3 audio content type.
|
|
207
|
+
* Used for MPEG audio file responses.
|
|
208
|
+
* @example
|
|
209
|
+
* headers.set("Content-Type", MP3);
|
|
210
|
+
* @example
|
|
211
|
+
* return new Response(mp3Buffer, { headers: { "Content-Type": MP3 } });
|
|
212
|
+
*/
|
|
213
|
+
export const MP3 = "audio/mpeg";
|
|
214
|
+
/**
|
|
215
|
+
* WAV audio content type.
|
|
216
|
+
* Used for uncompressed audio file responses.
|
|
217
|
+
* @example
|
|
218
|
+
* headers.set("Content-Type", WAV);
|
|
219
|
+
* @example
|
|
220
|
+
* return new Response(wavBuffer, { headers: { "Content-Type": WAV } });
|
|
221
|
+
*/
|
|
222
|
+
export const WAV = "audio/wav";
|
|
223
|
+
/**
|
|
224
|
+
* OGG audio content type.
|
|
225
|
+
* Used for Ogg Vorbis audio file responses.
|
|
226
|
+
* @example
|
|
227
|
+
* headers.set("Content-Type", OGG);
|
|
228
|
+
* @example
|
|
229
|
+
* return new Response(oggBuffer, { headers: { "Content-Type": OGG } });
|
|
230
|
+
*/
|
|
231
|
+
export const OGG = "audio/ogg";
|
|
232
|
+
/**
|
|
233
|
+
* WebM audio content type.
|
|
234
|
+
* Used for WebM audio-only file responses.
|
|
235
|
+
* @example
|
|
236
|
+
* headers.set("Content-Type", WebMAudio);
|
|
237
|
+
* @example
|
|
238
|
+
* return new Response(webmAudioBuffer, { headers: { "Content-Type": WebMAudio } });
|
|
239
|
+
*/
|
|
240
|
+
export const WebMAudio = "audio/webm";
|
|
241
|
+
/**
|
|
242
|
+
* MP4 video content type.
|
|
243
|
+
* Used for MPEG-4 video file responses.
|
|
244
|
+
* @example
|
|
245
|
+
* headers.set("Content-Type", MP4);
|
|
246
|
+
* @example
|
|
247
|
+
* return new Response(mp4Buffer, { headers: { "Content-Type": MP4 } });
|
|
248
|
+
*/
|
|
249
|
+
export const MP4 = "video/mp4";
|
|
250
|
+
/**
|
|
251
|
+
* WebM video content type.
|
|
252
|
+
* Used for WebM video file responses.
|
|
253
|
+
* @example
|
|
254
|
+
* headers.set("Content-Type", WebMVideo);
|
|
255
|
+
* @example
|
|
256
|
+
* return new Response(webmVideoBuffer, { headers: { "Content-Type": WebMVideo } });
|
|
257
|
+
*/
|
|
258
|
+
export const WebMVideo = "video/webm";
|
|
259
|
+
/**
|
|
260
|
+
* WOFF font content type.
|
|
261
|
+
* Used for Web Open Font Format file responses.
|
|
262
|
+
* @example
|
|
263
|
+
* headers.set("Content-Type", WOFF);
|
|
264
|
+
* @example
|
|
265
|
+
* return new Response(woffBuffer, { headers: { "Content-Type": WOFF } });
|
|
266
|
+
*/
|
|
267
|
+
export const WOFF = "font/woff";
|
|
268
|
+
/**
|
|
269
|
+
* WOFF2 font content type.
|
|
270
|
+
* Used for Web Open Font Format 2.0 file responses.
|
|
271
|
+
* @example
|
|
272
|
+
* headers.set("Content-Type", WOFF2);
|
|
273
|
+
* @example
|
|
274
|
+
* return new Response(woff2Buffer, { headers: { "Content-Type": WOFF2 } });
|
|
275
|
+
*/
|
|
276
|
+
export const WOFF2 = "font/woff2";
|
|
277
|
+
/**
|
|
278
|
+
* TrueType font content type.
|
|
279
|
+
* Used for TTF font file responses.
|
|
280
|
+
* @example
|
|
281
|
+
* headers.set("Content-Type", TTF);
|
|
282
|
+
* @example
|
|
283
|
+
* return new Response(ttfBuffer, { headers: { "Content-Type": TTF } });
|
|
284
|
+
*/
|
|
285
|
+
export const TTF = "font/ttf";
|
|
286
|
+
/**
|
|
287
|
+
* OpenType font content type.
|
|
288
|
+
* Used for OTF font file responses.
|
|
289
|
+
* @example
|
|
290
|
+
* headers.set("Content-Type", OTF);
|
|
291
|
+
* @example
|
|
292
|
+
* return new Response(otfBuffer, { headers: { "Content-Type": OTF } });
|
|
293
|
+
*/
|
|
294
|
+
export const OTF = "font/otf";
|
|
295
|
+
/**
|
|
296
|
+
* Server-Sent Events content type.
|
|
297
|
+
* Used for streaming event responses to clients.
|
|
298
|
+
* @example
|
|
299
|
+
* headers.set("Content-Type", EventStream);
|
|
300
|
+
* @example
|
|
301
|
+
* return new Response(stream, { headers: { "Content-Type": EventStream } });
|
|
302
|
+
*/
|
|
303
|
+
export const EventStream = "text/event-stream";
|
|
304
|
+
/**
|
|
305
|
+
* Newline-delimited JSON content type.
|
|
306
|
+
* Used for streaming JSON objects one per line.
|
|
307
|
+
* @example
|
|
308
|
+
* headers.set("Content-Type", NDJson);
|
|
309
|
+
* @example
|
|
310
|
+
* return new Response(ndjsonStream, { headers: { "Content-Type": NDJson } });
|
|
311
|
+
*/
|
|
312
|
+
export const NDJson = "application/x-ndjson";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router middleware that answers `HEAD` requests with the `GET` representation's
|
|
3
|
+
* status and headers and no body, as RFC 9110 §9.3.2 requires. The fetch router
|
|
4
|
+
* matches a route's method by strict equality, so without this a `HEAD` probe
|
|
5
|
+
* falls through to the default handler and a monitor reads a healthy page as a
|
|
6
|
+
* 404.
|
|
7
|
+
*
|
|
8
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
9
|
+
* @copyright Sergio Xalambrí 2026
|
|
10
|
+
*/
|
|
11
|
+
import type { Middleware } from "remix/router";
|
|
12
|
+
/**
|
|
13
|
+
* Creates a middleware that dispatches `HEAD` requests as `GET`, strips the
|
|
14
|
+
* response body, and keeps `HEAD` on the same chain as `GET` so auth, rate
|
|
15
|
+
* limiting, and other guards behave identically; install it first in the chain.
|
|
16
|
+
*
|
|
17
|
+
* @returns The middleware to place at the head of the router's global chain.
|
|
18
|
+
*/
|
|
19
|
+
export declare function headRequests(): Middleware;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router middleware that answers `HEAD` requests with the `GET` representation's
|
|
3
|
+
* status and headers and no body, as RFC 9110 §9.3.2 requires. The fetch router
|
|
4
|
+
* matches a route's method by strict equality, so without this a `HEAD` probe
|
|
5
|
+
* falls through to the default handler and a monitor reads a healthy page as a
|
|
6
|
+
* 404.
|
|
7
|
+
*
|
|
8
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
9
|
+
* @copyright Sergio Xalambrí 2026
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Creates a middleware that dispatches `HEAD` requests as `GET`, strips the
|
|
13
|
+
* response body, and keeps `HEAD` on the same chain as `GET` so auth, rate
|
|
14
|
+
* limiting, and other guards behave identically; install it first in the chain.
|
|
15
|
+
*
|
|
16
|
+
* @returns The middleware to place at the head of the router's global chain.
|
|
17
|
+
*/
|
|
18
|
+
export function headRequests() {
|
|
19
|
+
return async function headRequestsMiddleware(context, next) {
|
|
20
|
+
if (context.method !== "HEAD")
|
|
21
|
+
return next();
|
|
22
|
+
context.method = "GET";
|
|
23
|
+
let response = await next();
|
|
24
|
+
return new Response(null, {
|
|
25
|
+
status: response.status,
|
|
26
|
+
statusText: response.statusText,
|
|
27
|
+
headers: response.headers,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accept-header content negotiation: parse quality-ordered client
|
|
3
|
+
* preferences and pick the response representation that matches.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Holds an Accept header's types ordered by quality so callers can query
|
|
10
|
+
* preference without re-parsing on every check.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AcceptList {
|
|
13
|
+
private types;
|
|
14
|
+
constructor(header: string);
|
|
15
|
+
/**
|
|
16
|
+
* Matches shorthand types ("json", "html", "xml", "text", "markdown") as
|
|
17
|
+
* well as full MIME types, and treats a wildcard Accept header as
|
|
18
|
+
* matching any type.
|
|
19
|
+
* @param type - The content type or shorthand to check
|
|
20
|
+
* @returns True if the type is accepted
|
|
21
|
+
* @example
|
|
22
|
+
* if (accepts(request).includes("json")) { ... }
|
|
23
|
+
* @example
|
|
24
|
+
* if (accepts(request).includes("text/html")) { ... }
|
|
25
|
+
*/
|
|
26
|
+
includes(type: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Accepted types sorted by preference, highest quality first.
|
|
29
|
+
* @returns Array of MIME types sorted by preference
|
|
30
|
+
* @example
|
|
31
|
+
* let types = accepts(request).all(); // ["text/html", "application/json", ...]
|
|
32
|
+
*/
|
|
33
|
+
all(): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Walks the client's accepted types in preference order and returns the
|
|
36
|
+
* first one present in `types`; a wildcard Accept header matches the
|
|
37
|
+
* first of `types` rather than every candidate.
|
|
38
|
+
* @param types - Available content types to choose from
|
|
39
|
+
* @returns The preferred type or null
|
|
40
|
+
* @example
|
|
41
|
+
* let preferred = accepts(request).preferred("application/json", "text/html");
|
|
42
|
+
*/
|
|
43
|
+
preferred(...types: string[]): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Converts a MIME type to its shorthand form.
|
|
46
|
+
* @param mimeType - The full MIME type
|
|
47
|
+
* @returns The shorthand type or null if not recognized
|
|
48
|
+
*/
|
|
49
|
+
toShortType(mimeType: string): string | null;
|
|
50
|
+
private parseAcceptHeader;
|
|
51
|
+
private resolveType;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads the Accept header, defaulting to accepting every type when the
|
|
55
|
+
* client sends none.
|
|
56
|
+
* @param request - The incoming request
|
|
57
|
+
* @returns An AcceptList instance for querying accepted types
|
|
58
|
+
* @example
|
|
59
|
+
* if (accepts(request).includes("json")) return json(data);
|
|
60
|
+
* @example
|
|
61
|
+
* let types = accepts(request).all();
|
|
62
|
+
*/
|
|
63
|
+
export declare function accepts(request: Request): AcceptList;
|
|
64
|
+
/**
|
|
65
|
+
* Calls the handler for the client's preferred type, trying candidates in
|
|
66
|
+
* quality order; answers 406 Not Acceptable when nothing matches and no
|
|
67
|
+
* `default` handler is given.
|
|
68
|
+
* @param request - The incoming request
|
|
69
|
+
* @param handlers - Object mapping content types to response handlers
|
|
70
|
+
* @returns The response from the matching handler
|
|
71
|
+
* @example
|
|
72
|
+
* return respond(request, {
|
|
73
|
+
* json: () => json({ data }),
|
|
74
|
+
* html: () => html(renderPage(data)),
|
|
75
|
+
* });
|
|
76
|
+
* @example
|
|
77
|
+
* return respond(request, {
|
|
78
|
+
* json: () => json({ data }),
|
|
79
|
+
* default: () => json({ data }), // fallback
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
82
|
+
export declare function respond(request: Request, handlers: respond.Handlers): Response;
|
|
83
|
+
export declare namespace respond {
|
|
84
|
+
type Handlers = {
|
|
85
|
+
json?: () => Response;
|
|
86
|
+
html?: () => Response;
|
|
87
|
+
xml?: () => Response;
|
|
88
|
+
text?: () => Response;
|
|
89
|
+
markdown?: () => Response;
|
|
90
|
+
css?: () => Response;
|
|
91
|
+
javascript?: () => Response;
|
|
92
|
+
csv?: () => Response;
|
|
93
|
+
pdf?: () => Response;
|
|
94
|
+
default?: () => Response;
|
|
95
|
+
[key: string]: (() => Response) | undefined;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accept-header content negotiation: parse quality-ordered client
|
|
3
|
+
* preferences and pick the response representation that matches.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
const shortToMime = new Map([
|
|
9
|
+
["json", ["application/json"]],
|
|
10
|
+
["html", ["text/html"]],
|
|
11
|
+
["xml", ["application/xml", "text/xml"]],
|
|
12
|
+
["text", ["text/plain"]],
|
|
13
|
+
["markdown", ["text/markdown"]],
|
|
14
|
+
["css", ["text/css"]],
|
|
15
|
+
["javascript", ["text/javascript", "application/javascript"]],
|
|
16
|
+
["csv", ["text/csv"]],
|
|
17
|
+
["pdf", ["application/pdf"]],
|
|
18
|
+
]);
|
|
19
|
+
const mimeToShort = new Map([
|
|
20
|
+
["application/json", "json"],
|
|
21
|
+
["text/html", "html"],
|
|
22
|
+
["application/xml", "xml"],
|
|
23
|
+
["text/xml", "xml"],
|
|
24
|
+
["text/plain", "text"],
|
|
25
|
+
["text/markdown", "markdown"],
|
|
26
|
+
["text/css", "css"],
|
|
27
|
+
["text/javascript", "javascript"],
|
|
28
|
+
["application/javascript", "javascript"],
|
|
29
|
+
["text/csv", "csv"],
|
|
30
|
+
["application/pdf", "pdf"],
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Holds an Accept header's types ordered by quality so callers can query
|
|
34
|
+
* preference without re-parsing on every check.
|
|
35
|
+
*/
|
|
36
|
+
export class AcceptList {
|
|
37
|
+
types;
|
|
38
|
+
constructor(header) {
|
|
39
|
+
this.types = this.parseAcceptHeader(header);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Matches shorthand types ("json", "html", "xml", "text", "markdown") as
|
|
43
|
+
* well as full MIME types, and treats a wildcard Accept header as
|
|
44
|
+
* matching any type.
|
|
45
|
+
* @param type - The content type or shorthand to check
|
|
46
|
+
* @returns True if the type is accepted
|
|
47
|
+
* @example
|
|
48
|
+
* if (accepts(request).includes("json")) { ... }
|
|
49
|
+
* @example
|
|
50
|
+
* if (accepts(request).includes("text/html")) { ... }
|
|
51
|
+
*/
|
|
52
|
+
includes(type) {
|
|
53
|
+
let mimeTypes = this.resolveType(type);
|
|
54
|
+
return this.types.some((t) => mimeTypes.includes(t) || t === "*/*");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Accepted types sorted by preference, highest quality first.
|
|
58
|
+
* @returns Array of MIME types sorted by preference
|
|
59
|
+
* @example
|
|
60
|
+
* let types = accepts(request).all(); // ["text/html", "application/json", ...]
|
|
61
|
+
*/
|
|
62
|
+
all() {
|
|
63
|
+
return this.types;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Walks the client's accepted types in preference order and returns the
|
|
67
|
+
* first one present in `types`; a wildcard Accept header matches the
|
|
68
|
+
* first of `types` rather than every candidate.
|
|
69
|
+
* @param types - Available content types to choose from
|
|
70
|
+
* @returns The preferred type or null
|
|
71
|
+
* @example
|
|
72
|
+
* let preferred = accepts(request).preferred("application/json", "text/html");
|
|
73
|
+
*/
|
|
74
|
+
preferred(...types) {
|
|
75
|
+
for (let accepted of this.types) {
|
|
76
|
+
if (types.includes(accepted))
|
|
77
|
+
return accepted;
|
|
78
|
+
if (accepted === "*/*")
|
|
79
|
+
return types[0] ?? null;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Converts a MIME type to its shorthand form.
|
|
85
|
+
* @param mimeType - The full MIME type
|
|
86
|
+
* @returns The shorthand type or null if not recognized
|
|
87
|
+
*/
|
|
88
|
+
toShortType(mimeType) {
|
|
89
|
+
return mimeToShort.get(mimeType) ?? null;
|
|
90
|
+
}
|
|
91
|
+
parseAcceptHeader(header) {
|
|
92
|
+
return header
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((part) => {
|
|
95
|
+
let [type = "", ...params] = part.trim().split(";");
|
|
96
|
+
let q = 1;
|
|
97
|
+
for (let param of params) {
|
|
98
|
+
let [key, value = ""] = param.trim().split("=");
|
|
99
|
+
if (key === "q")
|
|
100
|
+
q = Number.parseFloat(value) || 0;
|
|
101
|
+
}
|
|
102
|
+
return { type: type.trim(), q };
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => b.q - a.q)
|
|
105
|
+
.map(({ type }) => type);
|
|
106
|
+
}
|
|
107
|
+
resolveType(type) {
|
|
108
|
+
return shortToMime.get(type) ?? [type];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reads the Accept header, defaulting to accepting every type when the
|
|
113
|
+
* client sends none.
|
|
114
|
+
* @param request - The incoming request
|
|
115
|
+
* @returns An AcceptList instance for querying accepted types
|
|
116
|
+
* @example
|
|
117
|
+
* if (accepts(request).includes("json")) return json(data);
|
|
118
|
+
* @example
|
|
119
|
+
* let types = accepts(request).all();
|
|
120
|
+
*/
|
|
121
|
+
export function accepts(request) {
|
|
122
|
+
let header = request.headers.get("Accept") ?? "*/*";
|
|
123
|
+
return new AcceptList(header);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Calls the handler for the client's preferred type, trying candidates in
|
|
127
|
+
* quality order; answers 406 Not Acceptable when nothing matches and no
|
|
128
|
+
* `default` handler is given.
|
|
129
|
+
* @param request - The incoming request
|
|
130
|
+
* @param handlers - Object mapping content types to response handlers
|
|
131
|
+
* @returns The response from the matching handler
|
|
132
|
+
* @example
|
|
133
|
+
* return respond(request, {
|
|
134
|
+
* json: () => json({ data }),
|
|
135
|
+
* html: () => html(renderPage(data)),
|
|
136
|
+
* });
|
|
137
|
+
* @example
|
|
138
|
+
* return respond(request, {
|
|
139
|
+
* json: () => json({ data }),
|
|
140
|
+
* default: () => json({ data }), // fallback
|
|
141
|
+
* });
|
|
142
|
+
*/
|
|
143
|
+
export function respond(request, handlers) {
|
|
144
|
+
let accept = accepts(request);
|
|
145
|
+
for (let type of accept.all()) {
|
|
146
|
+
let shortType = accept.toShortType(type);
|
|
147
|
+
if (shortType && handlers[shortType])
|
|
148
|
+
return handlers[shortType]();
|
|
149
|
+
}
|
|
150
|
+
if (handlers.default)
|
|
151
|
+
return handlers.default();
|
|
152
|
+
return new Response("Not Acceptable", {
|
|
153
|
+
status: 406,
|
|
154
|
+
statusText: "Not Acceptable",
|
|
155
|
+
});
|
|
156
|
+
}
|