@johntalton/http-util 2.0.0 → 2.0.3

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/README.md CHANGED
@@ -16,29 +16,89 @@ Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org
16
16
  - Forwarded - `parse` with select-right-most helper
17
17
  - Multipart - parse into `FormData`
18
18
  - Content Disposition - for use inside of Multipart
19
+ - Conditionals - Etag / FixDate for IfMatch, IfModifiedSince etc
19
20
 
20
21
  ### Server Sent:
21
22
  - Rate Limit
22
23
  - Server Timing
23
24
 
25
+
26
+ ```javascript
27
+ import {
28
+ Accept,
29
+ MIME_TYPE_JSON,
30
+ MIME_TYPE_TEXT
31
+ } from '@johntalton/http-util/headers'
32
+
33
+ // assuming our path/method/server supports content in json or text
34
+ const supportedType = [ MIME_TYPE_JSON, MIME_TYPE_TEXT ]
35
+
36
+ // from request.header.accept (client prefers json)
37
+ const acceptHeader = 'application/json;q=.5, */*;q.4'
38
+
39
+ const bestMatchingType = Accept.select(acceptHeader, supportedType)
40
+ // bestMatchingType === 'application/json'
41
+ ```
42
+
24
43
  ## Response
25
44
 
26
45
  All responders take in a `stream` as well as a metadata object to hint on servername and origin strings etc.
27
46
 
47
+ - `sendAccepted`
48
+ - `sendConflict`
49
+ - `sendCreated`
28
50
  - `sendError` - 500
29
- - `sendPreflight` - Response to OPTIONS with CORS headers
30
- - `sendUnauthorized` - Unauthorized
51
+ - `sendJSON_Encoded` - Standard Ok response with encoding
52
+ - `sendNoContent`
53
+ - `sendNotAcceptable`
54
+ - `sendNotAllowed` - Method not supported / allowed
31
55
  - `sendNotFound` - 404
56
+ - `sendNotModified`
57
+ - `sendPreflight` - Response to OPTIONS with CORS headers
58
+ - `sendTimeout`
32
59
  - `sendTooManyRequests` - Rate limit response (429)
33
- - `sendJSON_Encoded` - Standard Ok response with encoding
60
+ - `sendTrace`
61
+ - `sendUnauthorized` - Unauthorized
62
+ - `sendUnprocessable`
63
+ - `sendUnsupportedMediaType`
34
64
  - `sendSSE` - SSE header (leave the `stream` open)
35
65
 
36
66
  Responses allow for optional CORS headers as well as Server Timing meta data.
37
67
 
68
+ ## Response Object
69
+
70
+ The response methods `sendXYZ` are also wrapped in a `Response` object which can make imports simpler and help organize code.
71
+
72
+ ```js
73
+ import { Response } from '@johntalton/http-util/response/object'
74
+
75
+ // ... sendNotFound becomes .notFound
76
+ Response.notFound(stream, meta)
77
+ ```
78
+
38
79
  ## Body
39
80
 
40
81
  The `requestBody` method returns a `fetch`-like response. Including methods `blob`, `arrayBuffer`, `bytes`, `text`, `formData`, `json` as well as a `body` as a `ReadableStream`.
41
82
 
42
83
  The return is a deferred response that does NOT consume the `steam` until calling one of the above methods.
43
84
 
44
- Optional `byteLimit`, `contentLength` and `contentType` can be provided to hint the parser, as well as a `AbortSignal` to abandoned the reader.
85
+ Optional `byteLimit`, `contentLength` and `contentType` can be provided to hint the parser, as well as a `AbortSignal` to abandoned the reader.
86
+
87
+ ```js
88
+ import { requestBody } from '@johntalton/http-util/body'
89
+
90
+ const signal = // from someplace like a timeout for the overall request
91
+
92
+ // limit time and size for the body
93
+ // note: this does not consume the stream
94
+ const futureBody = requestBody(stream, {
95
+ byteLimit: 1000 * 1000,
96
+ signal
97
+ })
98
+
99
+ // ... a few moments later ...
100
+
101
+ // consume the stream
102
+ const body = futureBody.json()
103
+
104
+ ```
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
8
  "./headers": "./src/index.js",
9
9
  "./body": "./src/body.js",
10
- "./response": "./src/handle-stream-util.js"
10
+ "./response": "./src/response/index.js",
11
+ "./response/object": "./src/response/response.js"
11
12
  },
12
13
  "files": [
13
- "src/*.js"
14
+ "src/*.js",
15
+ "src/response/*.js"
14
16
  ],
15
17
  "repository": {
16
18
  "url": "https://github.com/johntalton/http-util"
package/src/body.js CHANGED
@@ -228,6 +228,9 @@ export async function bodyArrayBuffer(reader) {
228
228
  * @param {ReadableStream} reader
229
229
  */
230
230
  export async function bodyUint8Array(reader) {
231
+ // const blob = await bodyBlob(reader)
232
+ // return blob.bytes()
233
+
231
234
  const buffer = await bodyArrayBuffer(reader)
232
235
  return new Uint8Array(buffer)
233
236
 
@@ -0,0 +1,260 @@
1
+ export const CONDITION_ETAG_SEPARATOR = ','
2
+ export const CONDITION_ETAG_ANY = '*'
3
+ export const CONDITION_ETAG_WEAK_PREFIX = 'W/'
4
+ export const ETAG_QUOTE = '"'
5
+
6
+ export const DATE_SPACE = ' '
7
+ export const DATE_DAYS = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ]
8
+ export const DATE_MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
9
+ export const DATE_SEPARATOR = ','
10
+ export const DATE_TIME_SEPARATOR = ':'
11
+ export const DATE_ZONE = 'GMT'
12
+
13
+ /**
14
+ * @param {string} etag
15
+ */
16
+ export function isValidEtag(etag) {
17
+ // %x21 / %x23-7E and %x80-FF
18
+ for(const c of etag) {
19
+ if(c.charCodeAt(0) < 0x21) { return false }
20
+ if(c.charCodeAt(0) > 0xFF) { return false }
21
+ if(c === ETAG_QUOTE) { return false }
22
+ }
23
+ return true
24
+ }
25
+
26
+ /**
27
+ * @param {string} etag
28
+ */
29
+ export function stripQuotes(etag) {
30
+ return etag.substring(1, etag.length - 1)
31
+ }
32
+
33
+ /**
34
+ * @param {string} etag
35
+ */
36
+ export function isQuoted(etag) {
37
+ if(etag.length <= 2) { return false }
38
+ if(!etag.startsWith(ETAG_QUOTE)) { return false }
39
+ if(!etag.endsWith(ETAG_QUOTE)) { return false }
40
+ return true
41
+ }
42
+
43
+ /**
44
+ * @typedef {Object} EtagItem
45
+ * @property {boolean} weak
46
+ * @property {boolean} any
47
+ * @property {string} etag
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} IMFFixDate
52
+ * @property {typeof DATE_DAYS[number]} dayName
53
+ * @property {number} day
54
+ * @property {typeof DATE_MONTHS[number]} month
55
+ * @property {number} year
56
+ * @property {number} hour
57
+ * @property {number} minute
58
+ * @property {number} second
59
+ * @property {Date} date
60
+ */
61
+
62
+ export class Conditional {
63
+ /**
64
+ * @param {string|undefined} matchHeader
65
+ * @returns {Array<EtagItem>}
66
+ */
67
+ static parseEtagList(matchHeader) {
68
+ if(matchHeader === undefined) { return [] }
69
+
70
+ return matchHeader.split(CONDITION_ETAG_SEPARATOR)
71
+ .map(etag => etag.trim())
72
+ .map(etag => {
73
+ if(etag.startsWith(CONDITION_ETAG_WEAK_PREFIX)) {
74
+ // weak
75
+ return {
76
+ weak: true,
77
+ etag: etag.substring(CONDITION_ETAG_WEAK_PREFIX.length)
78
+ }
79
+ }
80
+
81
+ // strong
82
+ return {
83
+ weak: false,
84
+ etag
85
+ }
86
+ })
87
+ .map(item => {
88
+ if(item.etag === CONDITION_ETAG_ANY) {
89
+ return {
90
+ ...item,
91
+ any: true
92
+ }
93
+ }
94
+
95
+ // validated quoted
96
+ if(!isQuoted(item.etag)) { return undefined }
97
+ const etag = stripQuotes(item.etag)
98
+ if(!isValidEtag(etag)) { return undefined }
99
+ if(etag === CONDITION_ETAG_ANY) { return undefined }
100
+
101
+ return {
102
+ weak: item.weak,
103
+ any: false,
104
+ etag
105
+ }
106
+ })
107
+ .filter(item => item !== undefined)
108
+ }
109
+
110
+ /**
111
+ * @param {String|string|undefined} matchHeader
112
+ * @returns {IMFFixDate|undefined}
113
+ */
114
+ static parseFixDate(matchHeader) {
115
+ if(matchHeader === undefined || matchHeader === null) { return undefined }
116
+ // if(!(typeof matchHeader === 'string') && (!(matchHeader instanceof String))) { return undefined }
117
+
118
+ // https://www.rfc-editor.org/rfc/rfc5322.html#section-3.3
119
+ // https://httpwg.org/specs/rfc9110.html#preferred.date.format
120
+ // <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
121
+ // day-name "," SP date1 SP time-of-day SP GMT
122
+
123
+ if(matchHeader.length != 29) { return undefined }
124
+
125
+ //
126
+ const spaces = [
127
+ matchHeader.substring(4, 5),
128
+ matchHeader.substring(7, 8),
129
+ matchHeader.substring(11, 12),
130
+ matchHeader.substring(16, 17),
131
+ matchHeader.substring(25, 26)
132
+ ]
133
+ const comma = matchHeader.substring(3, 4)
134
+ const timeSeparators = [
135
+ matchHeader.substring(19, 20),
136
+ matchHeader.substring(22, 23)
137
+ ]
138
+ const gmt = matchHeader.substring(26)
139
+
140
+ //
141
+ if(comma !== DATE_SEPARATOR) { return undefined }
142
+ if(gmt !== DATE_ZONE) { return undefined }
143
+ for(const colon of timeSeparators) {
144
+ if(colon !== DATE_TIME_SEPARATOR) { return undefined }
145
+ }
146
+ for(const space of spaces) {
147
+ if(space !== DATE_SPACE) { return undefined }
148
+ }
149
+
150
+ //
151
+ const dayName = matchHeader.substring(0, 3)
152
+ const day = parseInt(matchHeader.substring(5, 7))
153
+ const month = matchHeader.substring(8, 11)
154
+ const year = parseInt(matchHeader.substring(12, 16))
155
+ const hour = parseInt(matchHeader.substring(17, 19))
156
+ const minute = parseInt(matchHeader.substring(20, 22))
157
+ const second = parseInt(matchHeader.substring(23, 25))
158
+
159
+ //
160
+ if(!DATE_DAYS.includes(dayName)) { return undefined }
161
+ if(!DATE_MONTHS.includes(month)) { return undefined }
162
+ if(!Number.isInteger(day)) { return undefined }
163
+ if(!Number.isInteger(year)) { return undefined }
164
+ if(!Number.isInteger(hour)) { return undefined }
165
+ if(!Number.isInteger(minute)) { return undefined }
166
+ if(!Number.isInteger(second)) { return undefined }
167
+
168
+ //
169
+ if(day > 31 || day <= 0) { return undefined }
170
+ if(year < 1900) { return undefined }
171
+ if(hour > 24 || hour < 0) { return undefined }
172
+ if(minute > 60 || minute < 0) { return undefined }
173
+ if(second > 60 || second < 0) { return undefined }
174
+
175
+ //
176
+ return {
177
+ dayName,
178
+ day,
179
+ month,
180
+ year,
181
+ hour,
182
+ minute,
183
+ second,
184
+ date: new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second)),
185
+ // temporal:
186
+ }
187
+ }
188
+ }
189
+
190
+ // Ok
191
+ // console.log(Conditional.parseEtagList('"bfc13a64729c4290ef5b2c2730249c88ca92d82d"'))
192
+ // console.log(Conditional.parseEtagList('W/"67ab43", "54ed21", "7892dd"'))
193
+ // console.log(Conditional.parseEtagList('*'))
194
+ // console.log(Conditional.parseEtagList('"!ÿ©"'))
195
+ // console.log(Conditional.parseEtagList('"!","ÿ", "©"'))
196
+ // console.log(Conditional.parseEtagList('"!","ÿ" ,\t"©"'))
197
+
198
+ // Error
199
+ console.log(Conditional.parseEtagList('"*"'))
200
+ // console.log(Conditional.parseEtagList('W/'))
201
+ // console.log(Conditional.parseEtagList('W/"'))
202
+ // console.log(Conditional.parseEtagList('W/""'))
203
+ // console.log(Conditional.parseEtagList(''))
204
+ // console.log(Conditional.parseEtagList('"'))
205
+ // console.log(Conditional.parseEtagList('""'))
206
+ // console.log(Conditional.parseEtagList('"""'))
207
+ // console.log(Conditional.parseEtagList('" "'))
208
+ // console.log(Conditional.parseEtagList('"\n"'))
209
+ // console.log(Conditional.parseEtagList('"\t"'))
210
+
211
+ //
212
+ // const testsOk = [
213
+ // 'Sun, 06 Nov 1994 08:49:37 GMT',
214
+ // 'Sun, 06 Nov 1994 00:00:00 GMT',
215
+ // 'Tue, 01 Nov 1994 00:00:00 GMT',
216
+ // 'Thu, 06 Nov 3000 08:49:37 GMT',
217
+ // 'Sun, 06 Nov 1994 23:59:59 GMT',
218
+ // new String('Sun, 06 Nov 1994 08:49:37 GMT'),
219
+ // ]
220
+ // for(const test of testsOk) {
221
+ // const result = Conditional.parseFixDate(test)
222
+ // if(result?.date.toUTCString() !== test.toString()) {
223
+ // console.log('🛑', test, result, result?.date.toUTCString())
224
+ // break
225
+ // }
226
+ // }
227
+
228
+ // const testBad = [
229
+ // undefined,
230
+ // null,
231
+ // {},
232
+ // new String(),
233
+ // '',
234
+ // 'Anything',
235
+ // ' , : : GMT',
236
+ // 'Sun, Nov : : GMT',
237
+ // 'Sun, 00 Nov 0000 00:00:00 GMT',
238
+ // 'Sun, 06 Nov 1994 08-49-37 GMT',
239
+ // 'Sun 06 Nov 1994 08:49:37 GMT',
240
+ // 'FOO, 06 Nov 1994 08:49:37 GMT',
241
+ // 'Sun, 32 Nov 1994 08:49:37 GMT',
242
+ // 'Sun, 00 Nov 1994 08:49:37 GMT',
243
+ // 'Sun, 06 Nov 0900 08:49:37 GMT',
244
+ // 'Sun, 06 Nov 1994 08:49:37 UTC',
245
+ // 'Sun, 06 Nov 1994 30:49:37 GMT',
246
+ // 'Sun,\t06 Nov 1994 08:49:37 GMT',
247
+
248
+ // 'Sunday, 06-Nov-94 08:49:37 GMT',
249
+ // 'Sun Nov 6 08:49:37 1994',
250
+ // 'Sun Nov 6 08:49:37 1994 ',
251
+
252
+ // ]
253
+ // for(const test of testBad) {
254
+ // const result = Conditional.parseFixDate(test)
255
+ // if(result !== undefined) {
256
+ // console.log('🛑', test, result)
257
+ // break
258
+ // }
259
+ // }
260
+
package/src/index.js CHANGED
@@ -2,9 +2,10 @@ export * from './accept-encoding.js'
2
2
  export * from './accept-language.js'
3
3
  export * from './accept-util.js'
4
4
  export * from './accept.js'
5
+ export * from './conditional.js'
5
6
  export * from './content-disposition.js'
6
7
  export * from './content-type.js'
7
8
  export * from './forwarded.js'
8
9
  export * from './multipart.js'
9
10
  export * from './rate-limit.js'
10
- export * from './server-timing.js'
11
+ export * from './server-timing.js'
@@ -0,0 +1,39 @@
1
+ import http2 from 'node:http2'
2
+ import { CONTENT_TYPE_JSON } from '../content-type.js'
3
+ import {
4
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
5
+ HTTP_HEADER_SERVER_TIMING,
6
+ ServerTiming
7
+ } from '../server-timing.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP_STATUS_ACCEPTED
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP2_HEADER_STATUS,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
20
+ } = http2.constants
21
+
22
+ /**
23
+ * @param {ServerHttp2Stream} stream
24
+ * @param {Metadata} meta
25
+ */
26
+ export function sendAccepted(stream, meta) {
27
+ stream.respond({
28
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
29
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_ACCEPTED,
30
+ // [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
31
+ [HTTP2_HEADER_SERVER]: meta.servername,
32
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
33
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
34
+ })
35
+
36
+ // stream.write(JSON.stringify( ... ))
37
+
38
+ stream.end()
39
+ }
@@ -0,0 +1,39 @@
1
+ import http2 from 'node:http2'
2
+ import { CONTENT_TYPE_JSON } from '../content-type.js'
3
+ import {
4
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
5
+ HTTP_HEADER_SERVER_TIMING,
6
+ ServerTiming
7
+ } from '../server-timing.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP_STATUS_CONFLICT
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP2_HEADER_STATUS,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
20
+ } = http2.constants
21
+
22
+ /**
23
+ * @param {ServerHttp2Stream} stream
24
+ * @param {Metadata} meta
25
+ */
26
+ export function sendConflict(stream, meta) {
27
+ stream.respond({
28
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
29
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_CONFLICT,
30
+ // [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
31
+ [HTTP2_HEADER_SERVER]: meta.servername,
32
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
33
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
34
+ })
35
+
36
+ // stream.write(JSON.stringify( ... ))
37
+
38
+ stream.end()
39
+ }
@@ -0,0 +1,42 @@
1
+ import http2 from 'node:http2'
2
+ import { CONTENT_TYPE_JSON } from '../content-type.js'
3
+ import {
4
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
5
+ HTTP_HEADER_SERVER_TIMING,
6
+ ServerTiming
7
+ } from '../server-timing.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP_STATUS_CREATED
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP2_HEADER_STATUS,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
20
+ HTTP2_HEADER_LOCATION
21
+ } = http2.constants
22
+
23
+ /**
24
+ * @param {ServerHttp2Stream} stream
25
+ * @param {URL} location
26
+ * @param {Metadata} meta
27
+ */
28
+ export function sendCreated(stream, location, meta) {
29
+ stream.respond({
30
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
31
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_CREATED,
32
+ // [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
33
+ [HTTP2_HEADER_SERVER]: meta.servername,
34
+ [HTTP2_HEADER_LOCATION]: location.href,
35
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
36
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
37
+ })
38
+
39
+ // stream.write(JSON.stringify( ... ))
40
+
41
+ stream.end()
42
+ }
@@ -0,0 +1,34 @@
1
+
2
+ export const HTTP_HEADER_ORIGIN = 'origin'
3
+ export const HTTP_HEADER_USER_AGENT = 'user-agent'
4
+ export const HTTP_HEADER_FORWARDED = 'forwarded'
5
+ export const HTTP_HEADER_SEC_CH_UA = 'sec-ch-ua'
6
+ export const HTTP_HEADER_SEC_CH_PLATFORM = 'sec-ch-ua-platform'
7
+ export const HTTP_HEADER_SEC_CH_MOBILE = 'sec-ch-ua-mobile'
8
+ export const HTTP_HEADER_SEC_FETCH_SITE = 'sec-fetch-site'
9
+ export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
10
+ export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
11
+ export const HTTP_HEADER_ACCEPT_POST = 'accept-post'
12
+
13
+ export const DEFAULT_METHODS = [ 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE' ]
14
+
15
+ export const HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE = 'access-control-max-age'
16
+ export const PREFLIGHT_AGE_SECONDS = '500'
17
+
18
+
19
+ /** @import { TimingsInfo } from '../server-timing.js' */
20
+
21
+ /**
22
+ * @typedef {Object} Metadata
23
+ * @property {Array<TimingsInfo>} performance
24
+ * @property {string|undefined} servername
25
+ * @property {string|undefined} origin
26
+ * @property {string|undefined} [etag]
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} SSEOptions
31
+ * @property {boolean} [active]
32
+ * @property {boolean} [bom]
33
+ */
34
+
@@ -0,0 +1,47 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ CONTENT_TYPE_TEXT
4
+ } from '../content-type.js'
5
+
6
+ const {
7
+ HTTP2_HEADER_STATUS,
8
+ HTTP2_HEADER_CONTENT_TYPE,
9
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
10
+ HTTP2_HEADER_SERVER
11
+ } = http2.constants
12
+
13
+ const {
14
+ HTTP_STATUS_INTERNAL_SERVER_ERROR
15
+ } = http2.constants
16
+
17
+ /** @import { ServerHttp2Stream } from 'node:http2' */
18
+ /** @import { Metadata } from './defs.js' */
19
+
20
+ /**
21
+ * @param {ServerHttp2Stream} stream
22
+ * @param {string} message
23
+ * @param {Metadata} meta
24
+ */
25
+ export function sendError(stream, message, meta) {
26
+ console.error('500', message)
27
+
28
+ if(stream === undefined) { return }
29
+ if(stream.closed) { return }
30
+
31
+ if(!stream.headersSent) {
32
+ stream.respond({
33
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
34
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_INTERNAL_SERVER_ERROR,
35
+ [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
36
+ [HTTP2_HEADER_SERVER]: meta.servername
37
+ })
38
+ }
39
+
40
+ // protect against HEAD calls
41
+ if(stream.writable) {
42
+ if(message !== undefined) { stream.write(message) }
43
+ }
44
+
45
+ stream.end()
46
+ if(!stream.closed) { stream.close() }
47
+ }
@@ -0,0 +1,20 @@
1
+ export * from './defs.js'
2
+
3
+ export * from './accepted.js'
4
+ export * from './conflict.js'
5
+ export * from './created.js'
6
+ export * from './error.js'
7
+ export * from './json.js'
8
+ export * from './no-content.js'
9
+ export * from './not-acceptable.js'
10
+ export * from './not-allowed.js'
11
+ export * from './not-found.js'
12
+ export * from './not-modified.js'
13
+ export * from './preflight.js'
14
+ export * from './sse.js'
15
+ export * from './timeout.js'
16
+ export * from './too-many-requests.js'
17
+ export * from './trace.js'
18
+ export * from './unauthorized.js'
19
+ export * from './unsupported-media.js'
20
+ export * from './unprocessable.js'