@johntalton/http-util 2.0.2 → 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 +64 -4
- package/package.json +1 -1
- package/src/body.js +3 -0
- package/src/conditional.js +260 -0
- package/src/index.js +2 -1
- package/src/response/accepted.js +5 -0
- package/src/response/conflict.js +39 -0
- package/src/response/created.js +5 -0
- package/src/response/error.js +0 -1
- package/src/response/index.js +4 -0
- package/src/response/no-content.js +36 -0
- package/src/response/not-allowed.js +2 -0
- package/src/response/not-found.js +1 -0
- package/src/response/response.js +9 -1
- package/src/response/timeout.js +41 -0
- package/src/response/too-many-requests.js +6 -2
- package/src/response/unauthorized.js +1 -0
- package/src/response/unprocessable.js +39 -0
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
|
-
- `
|
|
30
|
-
- `
|
|
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
|
-
- `
|
|
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
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'
|
package/src/response/accepted.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
2
3
|
import {
|
|
3
4
|
HTTP_HEADER_TIMING_ALLOW_ORIGIN,
|
|
4
5
|
HTTP_HEADER_SERVER_TIMING,
|
|
@@ -26,9 +27,13 @@ export function sendAccepted(stream, meta) {
|
|
|
26
27
|
stream.respond({
|
|
27
28
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
28
29
|
[HTTP2_HEADER_STATUS]: HTTP_STATUS_ACCEPTED,
|
|
30
|
+
// [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
|
|
29
31
|
[HTTP2_HEADER_SERVER]: meta.servername,
|
|
30
32
|
[HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
|
|
31
33
|
[HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
|
|
32
34
|
})
|
|
35
|
+
|
|
36
|
+
// stream.write(JSON.stringify( ... ))
|
|
37
|
+
|
|
33
38
|
stream.end()
|
|
34
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
|
+
}
|
package/src/response/created.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
2
3
|
import {
|
|
3
4
|
HTTP_HEADER_TIMING_ALLOW_ORIGIN,
|
|
4
5
|
HTTP_HEADER_SERVER_TIMING,
|
|
@@ -28,10 +29,14 @@ export function sendCreated(stream, location, meta) {
|
|
|
28
29
|
stream.respond({
|
|
29
30
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
30
31
|
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CREATED,
|
|
32
|
+
// [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
|
|
31
33
|
[HTTP2_HEADER_SERVER]: meta.servername,
|
|
32
34
|
[HTTP2_HEADER_LOCATION]: location.href,
|
|
33
35
|
[HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
|
|
34
36
|
[HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
|
|
35
37
|
})
|
|
38
|
+
|
|
39
|
+
// stream.write(JSON.stringify( ... ))
|
|
40
|
+
|
|
36
41
|
stream.end()
|
|
37
42
|
}
|
package/src/response/error.js
CHANGED
package/src/response/index.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
export * from './defs.js'
|
|
2
2
|
|
|
3
3
|
export * from './accepted.js'
|
|
4
|
+
export * from './conflict.js'
|
|
4
5
|
export * from './created.js'
|
|
5
6
|
export * from './error.js'
|
|
6
7
|
export * from './json.js'
|
|
8
|
+
export * from './no-content.js'
|
|
7
9
|
export * from './not-acceptable.js'
|
|
8
10
|
export * from './not-allowed.js'
|
|
9
11
|
export * from './not-found.js'
|
|
10
12
|
export * from './not-modified.js'
|
|
11
13
|
export * from './preflight.js'
|
|
12
14
|
export * from './sse.js'
|
|
15
|
+
export * from './timeout.js'
|
|
13
16
|
export * from './too-many-requests.js'
|
|
14
17
|
export * from './trace.js'
|
|
15
18
|
export * from './unauthorized.js'
|
|
16
19
|
export * from './unsupported-media.js'
|
|
20
|
+
export * from './unprocessable.js'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import {
|
|
3
|
+
HTTP_HEADER_TIMING_ALLOW_ORIGIN,
|
|
4
|
+
HTTP_HEADER_SERVER_TIMING,
|
|
5
|
+
ServerTiming
|
|
6
|
+
} from '../server-timing.js'
|
|
7
|
+
|
|
8
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
9
|
+
/** @import { Metadata } from './defs.js' */
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
HTTP_STATUS_NO_CONTENT
|
|
13
|
+
} = http2.constants
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
HTTP2_HEADER_STATUS,
|
|
17
|
+
HTTP2_HEADER_SERVER,
|
|
18
|
+
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
19
|
+
HTTP2_HEADER_ETAG
|
|
20
|
+
} = http2.constants
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {ServerHttp2Stream} stream
|
|
24
|
+
* @param {Metadata} meta
|
|
25
|
+
*/
|
|
26
|
+
export function sendNoContent(stream, meta) {
|
|
27
|
+
stream.respond({
|
|
28
|
+
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
29
|
+
[HTTP2_HEADER_STATUS]: HTTP_STATUS_NO_CONTENT,
|
|
30
|
+
[HTTP2_HEADER_SERVER]: meta.servername,
|
|
31
|
+
[HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
|
|
32
|
+
[HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
|
|
33
|
+
[HTTP2_HEADER_ETAG]: `"${meta.etag}"`
|
|
34
|
+
})
|
|
35
|
+
stream.end()
|
|
36
|
+
}
|
|
@@ -5,6 +5,7 @@ import http2 from 'node:http2'
|
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
7
|
HTTP2_HEADER_STATUS,
|
|
8
|
+
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
8
9
|
HTTP2_HEADER_SERVER,
|
|
9
10
|
HTTP2_HEADER_ALLOW
|
|
10
11
|
} = http2.constants
|
|
@@ -20,6 +21,7 @@ const {
|
|
|
20
21
|
*/
|
|
21
22
|
export function sendNotAllowed(stream, methods, meta) {
|
|
22
23
|
stream.respond({
|
|
24
|
+
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
23
25
|
[HTTP2_HEADER_STATUS]: HTTP_STATUS_METHOD_NOT_ALLOWED,
|
|
24
26
|
[HTTP2_HEADER_ALLOW]: methods.join(','),
|
|
25
27
|
[HTTP2_HEADER_SERVER]: meta.servername
|
package/src/response/response.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { sendAccepted } from './accepted.js'
|
|
2
|
+
import { sendConflict } from './conflict.js'
|
|
2
3
|
import { sendCreated } from './created.js'
|
|
3
4
|
import { sendError } from './error.js'
|
|
4
5
|
import { sendJSON_Encoded } from './json.js'
|
|
6
|
+
import { sendNoContent } from './no-content.js'
|
|
5
7
|
import { sendNotAcceptable } from './not-acceptable.js'
|
|
6
8
|
import { sendNotAllowed } from './not-allowed.js'
|
|
7
9
|
import { sendNotFound } from './not-found.js'
|
|
8
10
|
import { sendNotModified } from './not-modified.js'
|
|
9
11
|
import { sendPreflight } from './preflight.js'
|
|
10
12
|
import { sendSSE } from './sse.js'
|
|
13
|
+
import { sendTimeout } from './timeout.js'
|
|
11
14
|
import { sendTooManyRequests } from './too-many-requests.js'
|
|
12
15
|
import { sendTrace } from './trace.js'
|
|
13
16
|
import { sendUnauthorized } from './unauthorized.js'
|
|
14
17
|
import { sendUnsupportedMediaType } from './unsupported-media.js'
|
|
18
|
+
import { sendUnprocessable } from './unprocessable.js'
|
|
15
19
|
|
|
16
20
|
export const Response = {
|
|
17
21
|
accepted: sendAccepted,
|
|
22
|
+
conflict: sendConflict,
|
|
18
23
|
created: sendCreated,
|
|
19
24
|
error: sendError,
|
|
25
|
+
noContent: sendNoContent,
|
|
20
26
|
json: sendJSON_Encoded,
|
|
21
27
|
notAcceptable: sendNotAcceptable,
|
|
22
28
|
notAllowed: sendNotAllowed,
|
|
@@ -24,8 +30,10 @@ export const Response = {
|
|
|
24
30
|
notModified: sendNotModified,
|
|
25
31
|
preflight: sendPreflight,
|
|
26
32
|
sse: sendSSE,
|
|
33
|
+
timeout: sendTimeout,
|
|
27
34
|
tooManyRequests: sendTooManyRequests,
|
|
28
35
|
trace: sendTrace,
|
|
29
36
|
unauthorized: sendUnauthorized,
|
|
30
|
-
unsupportedMediaType: sendUnsupportedMediaType
|
|
37
|
+
unsupportedMediaType: sendUnsupportedMediaType,
|
|
38
|
+
unprocessable: sendUnprocessable
|
|
31
39
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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_REQUEST_TIMEOUT
|
|
14
|
+
} = http2.constants
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
HTTP2_HEADER_STATUS,
|
|
18
|
+
HTTP2_HEADER_SERVER,
|
|
19
|
+
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
20
|
+
HTTP2_HEADER_CONNECTION
|
|
21
|
+
} = http2.constants
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {ServerHttp2Stream} stream
|
|
25
|
+
* @param {Metadata} meta
|
|
26
|
+
*/
|
|
27
|
+
export function sendTimeout(stream, meta) {
|
|
28
|
+
stream.respond({
|
|
29
|
+
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
30
|
+
[HTTP2_HEADER_STATUS]: HTTP_STATUS_REQUEST_TIMEOUT,
|
|
31
|
+
// [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
|
|
32
|
+
[HTTP2_HEADER_SERVER]: meta.servername,
|
|
33
|
+
[HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
|
|
34
|
+
[HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
|
|
35
|
+
[HTTP2_HEADER_CONNECTION]: 'close'
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// stream.write(JSON.stringify( ... ))
|
|
39
|
+
|
|
40
|
+
stream.end()
|
|
41
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
import { CONTENT_TYPE_TEXT } from '../content-type.js'
|
|
2
3
|
import {
|
|
3
4
|
HTTP_HEADER_RATE_LIMIT,
|
|
4
5
|
HTTP_HEADER_RATE_LIMIT_POLICY,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
RateLimitPolicy
|
|
7
8
|
} from '../rate-limit.js'
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
10
12
|
/** @import { Metadata } from './defs.js' */
|
|
11
13
|
|
|
@@ -13,7 +15,8 @@ const {
|
|
|
13
15
|
HTTP2_HEADER_STATUS,
|
|
14
16
|
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
15
17
|
HTTP2_HEADER_SERVER,
|
|
16
|
-
HTTP2_HEADER_RETRY_AFTER
|
|
18
|
+
HTTP2_HEADER_RETRY_AFTER,
|
|
19
|
+
HTTP2_HEADER_CONTENT_TYPE
|
|
17
20
|
} = http2.constants
|
|
18
21
|
|
|
19
22
|
const {
|
|
@@ -30,14 +33,15 @@ export function sendTooManyRequests(stream, limitInfo, policies, meta) {
|
|
|
30
33
|
stream.respond({
|
|
31
34
|
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
32
35
|
[HTTP2_HEADER_STATUS]: HTTP_STATUS_TOO_MANY_REQUESTS,
|
|
36
|
+
[HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
|
|
33
37
|
[HTTP2_HEADER_SERVER]: meta.servername,
|
|
34
|
-
|
|
35
38
|
[HTTP2_HEADER_RETRY_AFTER]: limitInfo.retryAfterS,
|
|
36
39
|
[HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
|
|
37
40
|
[HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
|
|
38
41
|
})
|
|
39
42
|
|
|
40
43
|
stream.write(`Retry After ${limitInfo.retryAfterS} Seconds`)
|
|
44
|
+
|
|
41
45
|
stream.end()
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -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_UNPROCESSABLE_ENTITY
|
|
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 sendUnprocessable(stream, meta) {
|
|
27
|
+
stream.respond({
|
|
28
|
+
[HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
|
|
29
|
+
[HTTP2_HEADER_STATUS]: HTTP_STATUS_UNPROCESSABLE_ENTITY,
|
|
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
|
+
}
|