@johntalton/http-util 4.1.0 → 5.0.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/README.md +232 -21
- package/package.json +1 -1
- package/src/body.js +11 -11
- package/src/cache-control.js +6 -1
- package/src/conditional.js +26 -34
- package/src/content-range.js +59 -0
- package/src/content-type.js +2 -0
- package/src/index.js +3 -0
- package/src/multipart.js +79 -31
- package/src/range.js +159 -0
- package/src/response/bytes.js +26 -0
- package/src/response/content-too-large.js +15 -0
- package/src/response/defs.js +13 -1
- package/src/response/forbidden.js +17 -0
- package/src/response/gone.js +15 -0
- package/src/response/im-a-teapot.js +15 -0
- package/src/response/index.js +14 -0
- package/src/response/insufficient-storage.js +15 -0
- package/src/response/json.js +6 -53
- package/src/response/moved-permanently.js +22 -0
- package/src/response/multiple-choices.js +20 -0
- package/src/response/partial-content.js +71 -0
- package/src/response/permanent-redirect.js +23 -0
- package/src/response/preflight.js +20 -5
- package/src/response/range-not-satisfiable.js +27 -0
- package/src/response/response.js +26 -0
- package/src/response/see-other.js +22 -0
- package/src/response/send-util.js +128 -3
- package/src/response/temporary-redirect.js +22 -0
- package/src/response/unsupported-media.js +9 -4
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org/docs/latest/api/http2.html) stream compatible services.
|
|
4
4
|
|
|
5
|
-
- Header parsers
|
|
6
|
-
- Stream Response methods
|
|
7
|
-
- Body parser
|
|
5
|
+
- [Header parsers](#header-parsers)
|
|
6
|
+
- [Stream Response methods](#response)
|
|
7
|
+
- [Body parser](#body)
|
|
8
8
|
|
|
9
9
|
## Header Parsers
|
|
10
10
|
### From Client:
|
|
@@ -44,24 +44,37 @@ const bestMatchingType = Accept.select(acceptHeader, supportedType)
|
|
|
44
44
|
|
|
45
45
|
All responders take in a `stream` as well as a metadata object to hint on servername and origin strings etc.
|
|
46
46
|
|
|
47
|
-
- `sendAccepted`
|
|
48
|
-
- `sendConflict`
|
|
49
|
-
- `
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
47
|
+
- [`sendAccepted`](#responseaccepted)
|
|
48
|
+
- [`sendConflict`](#responseconflict)
|
|
49
|
+
- [`sendContentTooLarge`](#)
|
|
50
|
+
- [`sendCreated`](#responsecreated)
|
|
51
|
+
- [`sendError`](#responseerror) - 500
|
|
52
|
+
- [`sendGone`](#)
|
|
53
|
+
- [`sendImATeapot`](#)
|
|
54
|
+
- [`sendInsufficientStorage`](#)
|
|
55
|
+
- [`sendJSON_Encoded`](#responsejson) - Standard Ok response with encoding
|
|
56
|
+
- [`sendMovedPermanently`](#)
|
|
57
|
+
- [`sendMultipleChoices`](#)
|
|
58
|
+
- [`sendNoContent`](#responsenocontent)
|
|
59
|
+
- [`sendNotAcceptable`](#responsenotacceptable)
|
|
60
|
+
- [`sendNotAllowed`](#responsenotallowed) - Method not supported / allowed
|
|
61
|
+
- [`sendNotFound`](#responsenotfound) - 404
|
|
62
|
+
- [`sendNotImplemented`](#responsenotimplemented)
|
|
63
|
+
- [`sendNotModified`](#responsenotmodified)
|
|
64
|
+
- [`sendPartialContent`](#)
|
|
65
|
+
- [`sendPreconditionFailed`](#responsepreconditionfailed)
|
|
66
|
+
- [`sendPreflight`](#responsepreflight) - Response to OPTIONS with CORS headers
|
|
67
|
+
- [`sendRangeNotSatisfiable`](#)
|
|
68
|
+
- [`sendSeeOther`](#)
|
|
69
|
+
- [`sendTemporaryRedirect`](#)
|
|
70
|
+
- [`sendTimeout`](#responsetimeout)
|
|
71
|
+
- [`sendTooManyRequests`](#responsetoomanyrequests) - Rate limit response (429)
|
|
72
|
+
- [`sendTrace`](#responsetrace)
|
|
73
|
+
- [`sendUnauthorized`](#responseunauthorized) - Unauthorized
|
|
74
|
+
- [`sendUnavailable`](#responseunavailable)
|
|
75
|
+
- [`sendUnprocessable`](#responseunprocessable)
|
|
76
|
+
- [`sendUnsupportedMediaType`](#responseunsupportedmediatype)
|
|
77
|
+
- [`sendSSE`](#responsesse) - SSE header (leave the `stream` open)
|
|
65
78
|
|
|
66
79
|
Responses allow for optional CORS headers as well as Server Timing meta data.
|
|
67
80
|
|
|
@@ -102,3 +115,201 @@ const futureBody = requestBody(stream, {
|
|
|
102
115
|
const body = futureBody.json()
|
|
103
116
|
|
|
104
117
|
```
|
|
118
|
+
|
|
119
|
+
## Response method API
|
|
120
|
+
|
|
121
|
+
All response methods take in the `stream` and a `meta` property.
|
|
122
|
+
|
|
123
|
+
Additional parameters are specific to each return type.
|
|
124
|
+
|
|
125
|
+
Each return type semantic may also expose header (in addition to the standard headers) to the calling code as needed.
|
|
126
|
+
|
|
127
|
+
Some common objects can be passed such as `EtagItem`, `CacheControlOptions` and `Metadata` have their own structure.
|
|
128
|
+
|
|
129
|
+
Many parameters accept `undefined` to skip/ignore the usage
|
|
130
|
+
|
|
131
|
+
### Response.accepted
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
- stream
|
|
135
|
+
- meta
|
|
136
|
+
|
|
137
|
+
### Response.conflict
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
- stream
|
|
141
|
+
- meta
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
### Response.created
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
- stream
|
|
148
|
+
- location
|
|
149
|
+
- etag
|
|
150
|
+
- meta
|
|
151
|
+
|
|
152
|
+
Additional Exposed Headers:
|
|
153
|
+
- location
|
|
154
|
+
|
|
155
|
+
### Response.error
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
- stream
|
|
159
|
+
- meta
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
### Response.json
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
- stream
|
|
166
|
+
- object
|
|
167
|
+
- encoding - undefined | 'identity' | 'br' | 'gzip' | 'deflate' | 'zstd'
|
|
168
|
+
- etag
|
|
169
|
+
- age
|
|
170
|
+
- cacheControl
|
|
171
|
+
- supportedQueryTypes - undefined | Array of supported query types
|
|
172
|
+
- meta
|
|
173
|
+
|
|
174
|
+
Additional Exposed Headers
|
|
175
|
+
- age
|
|
176
|
+
- accept-query
|
|
177
|
+
|
|
178
|
+
### Response.noContent
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
- stream
|
|
182
|
+
- etag
|
|
183
|
+
- meta
|
|
184
|
+
|
|
185
|
+
### Response.notAcceptable
|
|
186
|
+
|
|
187
|
+
Parameters:
|
|
188
|
+
- stream
|
|
189
|
+
- supportedTypes
|
|
190
|
+
- meta
|
|
191
|
+
|
|
192
|
+
### Response.notAllowed
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
- stream
|
|
196
|
+
- methods - Array of allowed methods
|
|
197
|
+
- meta
|
|
198
|
+
|
|
199
|
+
Additional Exposed Headers:
|
|
200
|
+
- allow
|
|
201
|
+
|
|
202
|
+
### Response.notFound
|
|
203
|
+
|
|
204
|
+
Parameters:
|
|
205
|
+
- stream
|
|
206
|
+
- message
|
|
207
|
+
- meta
|
|
208
|
+
|
|
209
|
+
### Response.notImplemented
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
- stream
|
|
213
|
+
- message
|
|
214
|
+
- meta
|
|
215
|
+
|
|
216
|
+
### Response.notModified
|
|
217
|
+
|
|
218
|
+
Parameters:
|
|
219
|
+
- stream
|
|
220
|
+
- etag
|
|
221
|
+
- age
|
|
222
|
+
- cacheControl
|
|
223
|
+
- meta
|
|
224
|
+
|
|
225
|
+
Additional Exposed Headers:
|
|
226
|
+
- age
|
|
227
|
+
|
|
228
|
+
### Response.preconditionFailed
|
|
229
|
+
|
|
230
|
+
Parameters:
|
|
231
|
+
- stream
|
|
232
|
+
- meta
|
|
233
|
+
|
|
234
|
+
### Response.preflight
|
|
235
|
+
|
|
236
|
+
Parameters:
|
|
237
|
+
- stream
|
|
238
|
+
- methods
|
|
239
|
+
- supportedQueryTypes - undefined | Array of supported types
|
|
240
|
+
- meta
|
|
241
|
+
|
|
242
|
+
Additional Exposed Headers:
|
|
243
|
+
- accept-query
|
|
244
|
+
|
|
245
|
+
### Response.sse
|
|
246
|
+
|
|
247
|
+
Parameters:
|
|
248
|
+
- stream
|
|
249
|
+
- meta
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
### Response.timeout
|
|
253
|
+
|
|
254
|
+
Parameters:
|
|
255
|
+
- stream
|
|
256
|
+
- meta
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
### Response.tooManyRequests
|
|
260
|
+
|
|
261
|
+
Parameters:
|
|
262
|
+
- stream
|
|
263
|
+
- limitInfo - `RateLimitInfo`
|
|
264
|
+
- policies - Array of `RateLimitPolicyInfo`
|
|
265
|
+
- meta
|
|
266
|
+
|
|
267
|
+
Additional Exposed Headers:
|
|
268
|
+
- retry-after
|
|
269
|
+
- rate-limit
|
|
270
|
+
- rate-limit-policy
|
|
271
|
+
|
|
272
|
+
### Response.trace
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
- stream
|
|
276
|
+
- method
|
|
277
|
+
- url
|
|
278
|
+
- headers
|
|
279
|
+
- meta
|
|
280
|
+
|
|
281
|
+
### Response.unauthorized
|
|
282
|
+
|
|
283
|
+
Parameters:
|
|
284
|
+
- stream
|
|
285
|
+
- meta
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
### Response.unavailable
|
|
289
|
+
|
|
290
|
+
Parameters:
|
|
291
|
+
- stream
|
|
292
|
+
- message
|
|
293
|
+
- retryAfter
|
|
294
|
+
- meta
|
|
295
|
+
|
|
296
|
+
Additional Exposed Headers:
|
|
297
|
+
- retry-after
|
|
298
|
+
|
|
299
|
+
### Response.unprocessable
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
- stream
|
|
303
|
+
- meta
|
|
304
|
+
|
|
305
|
+
### Response.unsupportedMediaType
|
|
306
|
+
|
|
307
|
+
Parameters:
|
|
308
|
+
- stream
|
|
309
|
+
- acceptableMediaTypes
|
|
310
|
+
- supportedQueryTypes
|
|
311
|
+
- meta
|
|
312
|
+
|
|
313
|
+
Additional Exposed Headers:
|
|
314
|
+
- accept-query
|
|
315
|
+
- accept-post
|
package/package.json
CHANGED
package/src/body.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CHARSET_UTF8,
|
|
3
|
+
MIME_TYPE_MULTIPART_FORM_DATA,
|
|
4
|
+
MIME_TYPE_URL_FORM_DATA
|
|
5
|
+
} from './content-type.js'
|
|
2
6
|
import { Multipart } from './multipart.js'
|
|
3
7
|
|
|
4
8
|
export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
|
|
5
9
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @import { ContentType } from './content-type.js'
|
|
12
|
-
*/
|
|
10
|
+
/** @import { Readable } from 'node:stream' */
|
|
11
|
+
/** @import { ContentType } from './content-type.js' */
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* @typedef {Object} BodyOptions
|
|
@@ -32,7 +31,6 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
|
|
|
32
31
|
* @property { () => Promise<any> } json
|
|
33
32
|
*/
|
|
34
33
|
|
|
35
|
-
|
|
36
34
|
/**
|
|
37
35
|
* @param {Readable} stream
|
|
38
36
|
* @param {BodyOptions} [options]
|
|
@@ -92,9 +90,9 @@ export function requestBody(stream, options) {
|
|
|
92
90
|
// return
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
|
|
96
93
|
const listener = () => {
|
|
97
|
-
|
|
94
|
+
stats.closed = true
|
|
95
|
+
controller.error(new Error('Abort Signal'))
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
signal?.addEventListener('abort', listener, { once: true })
|
|
@@ -146,6 +144,8 @@ export function requestBody(stream, options) {
|
|
|
146
144
|
|
|
147
145
|
stream.on('close', () => {
|
|
148
146
|
// console.log('body reader stream close')
|
|
147
|
+
signal?.removeEventListener('abort', listener)
|
|
148
|
+
|
|
149
149
|
if(!stats.closed) {
|
|
150
150
|
stats.closed = true
|
|
151
151
|
controller.close()
|
package/src/cache-control.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
export class CacheControl {
|
|
14
14
|
/**
|
|
15
|
-
* @param {CacheControlOptions} options
|
|
15
|
+
* @param {CacheControlOptions|undefined} options
|
|
16
16
|
* @returns {string|undefined}
|
|
17
17
|
*/
|
|
18
18
|
static encode(options) {
|
|
19
|
+
if(options === undefined) { return undefined }
|
|
20
|
+
|
|
19
21
|
const {
|
|
20
22
|
pub,
|
|
21
23
|
priv,
|
|
@@ -46,6 +48,9 @@ export class CacheControl {
|
|
|
46
48
|
result.push(`stale-if-error=${staleIfError}`)
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
//
|
|
52
|
+
if(result.length === 0) { return undefined }
|
|
53
|
+
|
|
49
54
|
return result.join(', ')
|
|
50
55
|
}
|
|
51
56
|
}
|
package/src/conditional.js
CHANGED
|
@@ -102,6 +102,31 @@ export class ETag {
|
|
|
102
102
|
* @returns {AnyEtagItem}
|
|
103
103
|
*/
|
|
104
104
|
static any() { return ANY_ETAG_ITEM }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string|undefined} raw
|
|
108
|
+
* @returns {EtagItem|undefined}
|
|
109
|
+
*/
|
|
110
|
+
static parse(raw) {
|
|
111
|
+
if(raw === undefined) { return undefined }
|
|
112
|
+
|
|
113
|
+
const rawEtag = raw.trim()
|
|
114
|
+
const weak = rawEtag.startsWith(CONDITION_ETAG_WEAK_PREFIX)
|
|
115
|
+
const quotedEtag = weak ? rawEtag.substring(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
|
|
116
|
+
|
|
117
|
+
if(quotedEtag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
118
|
+
|
|
119
|
+
if(!isQuoted(quotedEtag)) { return undefined }
|
|
120
|
+
const etag = stripQuotes(quotedEtag)
|
|
121
|
+
if(!isValidEtag(etag)) { return undefined }
|
|
122
|
+
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
weak,
|
|
126
|
+
any: false,
|
|
127
|
+
etag
|
|
128
|
+
}
|
|
129
|
+
}
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
export class Conditional {
|
|
@@ -131,40 +156,7 @@ export class Conditional {
|
|
|
131
156
|
if(matchHeader === undefined) { return [] }
|
|
132
157
|
|
|
133
158
|
return matchHeader.split(CONDITION_ETAG_SEPARATOR)
|
|
134
|
-
.map(
|
|
135
|
-
.map(etag => {
|
|
136
|
-
if(etag.startsWith(CONDITION_ETAG_WEAK_PREFIX)) {
|
|
137
|
-
// weak
|
|
138
|
-
return {
|
|
139
|
-
weak: true,
|
|
140
|
-
etag: etag.substring(CONDITION_ETAG_WEAK_PREFIX.length)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// strong
|
|
145
|
-
return {
|
|
146
|
-
weak: false,
|
|
147
|
-
etag
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
.map(item => {
|
|
151
|
-
if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
152
|
-
|
|
153
|
-
// validated quoted
|
|
154
|
-
if(!isQuoted(item.etag)) { return undefined }
|
|
155
|
-
const etag = stripQuotes(item.etag)
|
|
156
|
-
if(!isValidEtag(etag)) { return undefined }
|
|
157
|
-
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
158
|
-
|
|
159
|
-
/** @type {WeakEtagItem | NotWeakEtagItem} */
|
|
160
|
-
const result = {
|
|
161
|
-
weak: item.weak,
|
|
162
|
-
any: false,
|
|
163
|
-
etag
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
})
|
|
159
|
+
.map(ETag.parse)
|
|
168
160
|
.filter(item => item !== undefined)
|
|
169
161
|
}
|
|
170
162
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RANGE_UNITS_BYTES } from "./response/defs.js"
|
|
2
|
+
|
|
3
|
+
export const CONTENT_RANGE_UNKNOWN = '*'
|
|
4
|
+
export const CONTENT_RANGE_SEPARATOR = '-'
|
|
5
|
+
export const CONTENT_RANGE_SIZE_SEPARATOR = '/'
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CONTENT_RANGE_DIRECTIVE = {
|
|
8
|
+
units: RANGE_UNITS_BYTES,
|
|
9
|
+
range: CONTENT_RANGE_UNKNOWN,
|
|
10
|
+
size: CONTENT_RANGE_UNKNOWN
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ContentRangeDirective
|
|
14
|
+
* @property {RANGE_UNITS_BYTES|undefined} [units]
|
|
15
|
+
* @property {{ start: number, end: number }|CONTENT_RANGE_UNKNOWN|undefined} [range]
|
|
16
|
+
* @property {number|CONTENT_RANGE_UNKNOWN|undefined} [size]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class ContentRange {
|
|
20
|
+
/**
|
|
21
|
+
* @param {ContentRangeDirective|undefined} rangeDirective
|
|
22
|
+
* @returns {string|undefined}
|
|
23
|
+
*/
|
|
24
|
+
static encode(rangeDirective) {
|
|
25
|
+
if(rangeDirective === undefined) { return undefined }
|
|
26
|
+
|
|
27
|
+
const units = rangeDirective.units ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.units
|
|
28
|
+
const size = rangeDirective.size ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.size
|
|
29
|
+
const range = rangeDirective.range ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.range
|
|
30
|
+
|
|
31
|
+
if(units !== RANGE_UNITS_BYTES) { return undefined }
|
|
32
|
+
if(size !== CONTENT_RANGE_UNKNOWN && !Number.isInteger(size)) { return undefined }
|
|
33
|
+
|
|
34
|
+
if((typeof range === 'string')) {
|
|
35
|
+
if(range !== CONTENT_RANGE_UNKNOWN) { return undefined }
|
|
36
|
+
return `${units} ${CONTENT_RANGE_UNKNOWN}${CONTENT_RANGE_SIZE_SEPARATOR}${size}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rangeStr = `${range.start}${CONTENT_RANGE_SEPARATOR}${range.end}`
|
|
40
|
+
return `${units} ${rangeStr}${CONTENT_RANGE_SIZE_SEPARATOR}${size}`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// console.log(ContentRange.encode({}))
|
|
45
|
+
// console.log(ContentRange.encode({ size: '*' }))
|
|
46
|
+
// console.log(ContentRange.encode({ units: 'bytes' }))
|
|
47
|
+
// console.log(ContentRange.encode({ range: '*' }))
|
|
48
|
+
// console.log(ContentRange.encode({ range: '*', size: '*' }))
|
|
49
|
+
|
|
50
|
+
// console.log()
|
|
51
|
+
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 } }))
|
|
52
|
+
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 }, size: 1024 }))
|
|
53
|
+
// console.log(ContentRange.encode({ range: '*', size: 1024 }))
|
|
54
|
+
// console.log(ContentRange.encode({ size: 1024 }))
|
|
55
|
+
|
|
56
|
+
// console.log()
|
|
57
|
+
// console.log(ContentRange.encode({ units: 'bob' }))
|
|
58
|
+
// console.log(ContentRange.encode({ range: 'bob' }))
|
|
59
|
+
// console.log(ContentRange.encode({ size: 'bob' }))
|
package/src/content-type.js
CHANGED
|
@@ -5,9 +5,11 @@ export const MIME_TYPE_EVENT_STREAM = 'text/event-stream'
|
|
|
5
5
|
export const MIME_TYPE_XML = 'application/xml'
|
|
6
6
|
export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
|
|
7
7
|
export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
|
|
8
|
+
export const MIME_TYPE_MULTIPART_RANGE = 'multipart/byteranges'
|
|
8
9
|
export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
|
|
9
10
|
export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
|
|
10
11
|
|
|
12
|
+
|
|
11
13
|
export const KNOWN_CONTENT_TYPES = [
|
|
12
14
|
'application', 'audio', 'image', 'message',
|
|
13
15
|
'multipart','text', 'video'
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,13 @@ 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 './cache-control.js'
|
|
5
6
|
export * from './conditional.js'
|
|
6
7
|
export * from './content-disposition.js'
|
|
8
|
+
export * from './content-range.js'
|
|
7
9
|
export * from './content-type.js'
|
|
8
10
|
export * from './forwarded.js'
|
|
9
11
|
export * from './multipart.js'
|
|
12
|
+
export * from './range.js'
|
|
10
13
|
export * from './rate-limit.js'
|
|
11
14
|
export * from './server-timing.js'
|
package/src/multipart.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
import { ReadableStream } from 'node:stream/web'
|
|
2
|
+
|
|
1
3
|
import { parseContentDisposition } from './content-disposition.js'
|
|
2
4
|
import { parseContentType } from './content-type.js'
|
|
5
|
+
import { ContentRange } from './content-range.js'
|
|
6
|
+
|
|
7
|
+
/** @import { ContentRangeDirective } from './content-range.js' */
|
|
8
|
+
/** @import { SendBody } from './response/send-util.js' */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} MultipartBytePart
|
|
12
|
+
* @property {SendBody} obj
|
|
13
|
+
* @property {ContentRangeDirective} range
|
|
14
|
+
*/
|
|
3
15
|
|
|
4
16
|
export const DISPOSITION_FORM_DATA = 'form-data'
|
|
5
17
|
|
|
@@ -12,7 +24,8 @@ export const EMPTY = ''
|
|
|
12
24
|
|
|
13
25
|
export const MULTIPART_HEADER = {
|
|
14
26
|
CONTENT_DISPOSITION: 'content-disposition',
|
|
15
|
-
CONTENT_TYPE: 'content-type'
|
|
27
|
+
CONTENT_TYPE: 'content-type',
|
|
28
|
+
CONTENT_RANGE: 'content-range'
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
export const MULTIPART_STATE = {
|
|
@@ -29,6 +42,15 @@ export class Multipart {
|
|
|
29
42
|
* @param {string} [charset='utf8']
|
|
30
43
|
*/
|
|
31
44
|
static parse(text, boundary, charset = 'utf8') {
|
|
45
|
+
return Multipart.parse_FormData(text, boundary, charset)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @param {string} boundary
|
|
51
|
+
* @param {string} [charset='utf8']
|
|
52
|
+
*/
|
|
53
|
+
static parse_FormData(text, boundary, charset = 'utf8') {
|
|
32
54
|
// console.log({ boundary, text })
|
|
33
55
|
const formData = new FormData()
|
|
34
56
|
|
|
@@ -114,36 +136,62 @@ export class Multipart {
|
|
|
114
136
|
|
|
115
137
|
return formData
|
|
116
138
|
}
|
|
117
|
-
}
|
|
118
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} contentType
|
|
142
|
+
* @param {Array<MultipartBytePart>} parts
|
|
143
|
+
* @param {number|undefined} contentLength
|
|
144
|
+
* @param {string} boundary
|
|
145
|
+
* @returns {ReadableStream<Uint8Array>}
|
|
146
|
+
*/
|
|
147
|
+
static encode_Bytes(contentType, parts, contentLength, boundary) {
|
|
148
|
+
const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
|
|
149
|
+
const boundaryEnd = `${BOUNDARY_MARK}${boundary}${BOUNDARY_MARK}`
|
|
150
|
+
|
|
151
|
+
return new ReadableStream({
|
|
152
|
+
type: 'bytes',
|
|
153
|
+
async start(controller) {
|
|
154
|
+
const encoder = new TextEncoder()
|
|
155
|
+
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
controller.enqueue(encoder.encode(`${boundaryBegin}${MULTIPART_SEPARATOR}`))
|
|
158
|
+
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
|
|
159
|
+
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
|
|
160
|
+
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
161
|
+
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
162
|
+
|
|
163
|
+
if(part.obj instanceof ReadableStream) {
|
|
164
|
+
for await (const chunk of part.obj) {
|
|
165
|
+
if(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
|
|
166
|
+
controller.enqueue(chunk)
|
|
167
|
+
}
|
|
168
|
+
else if(typeof chunk === 'string'){
|
|
169
|
+
controller.enqueue(encoder.encode(chunk))
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// console.log('chunk type', typeof chunk)
|
|
173
|
+
controller.enqueue(Uint8Array.from([ chunk ]))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if(part.obj instanceof ArrayBuffer || ArrayBuffer.isView(part.obj)) {
|
|
178
|
+
controller.enqueue(part.obj)
|
|
179
|
+
}
|
|
180
|
+
else if(typeof part.obj === 'string'){
|
|
181
|
+
controller.enqueue(encoder.encode(part.obj))
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// console.log('error', typeof part.obj, part.obj)
|
|
185
|
+
throw new Error('unknown part type')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
189
|
+
}
|
|
119
190
|
|
|
191
|
+
controller.enqueue(encoder.encode(boundaryEnd))
|
|
120
192
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// 'Content-Disposition: form-data; name="user"\r\n' +
|
|
127
|
-
// '\r\n' +
|
|
128
|
-
// 'jeff\r\n' +
|
|
129
|
-
// '--X-INSOMNIA-BOUNDARY--\r\n'
|
|
130
|
-
// const result = Multipart.parse(test, 'X-INSOMNIA-BOUNDARY')
|
|
131
|
-
// console.log(result)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// const test = [
|
|
135
|
-
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
|
|
136
|
-
// 'Content-Disposition: form-data; '
|
|
137
|
-
// + 'name="upload_file_0"; filename="テスト.dat"',
|
|
138
|
-
// 'Content-Type: application/octet-stream',
|
|
139
|
-
// '',
|
|
140
|
-
// 'A'.repeat(1023),
|
|
141
|
-
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
|
|
142
|
-
// ].join('\r\n')
|
|
143
|
-
// const result = Multipart.parse(test, '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k')
|
|
144
|
-
// console.log(result)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// const test = '--X-INSOMNIA-BOUNDARY--\r\n'
|
|
148
|
-
// const result = Multipart.parse(test, 'X-INSOMNIA-BOUNDARY')
|
|
149
|
-
// console.log(result)
|
|
193
|
+
controller.close()
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|