@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/src/response/response.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { sendAccepted } from './accepted.js'
|
|
2
|
+
import { sendBytes } from './bytes.js'
|
|
2
3
|
import { sendConflict } from './conflict.js'
|
|
4
|
+
import { sendContentTooLarge } from './content-too-large.js'
|
|
3
5
|
import { sendCreated } from './created.js'
|
|
4
6
|
import { sendError } from './error.js'
|
|
7
|
+
import { sendForbidden } from './forbidden.js'
|
|
8
|
+
import { sendGone } from './gone.js'
|
|
9
|
+
import { sendImATeapot } from './im-a-teapot.js'
|
|
10
|
+
import { sendInsufficientStorage } from './insufficient-storage.js'
|
|
5
11
|
import { sendJSON_Encoded } from './json.js'
|
|
12
|
+
import { sendMovedPermanently } from './moved-permanently.js'
|
|
13
|
+
import { sendMultipleChoices } from './multiple-choices.js'
|
|
6
14
|
import { sendNoContent } from './no-content.js'
|
|
7
15
|
import { sendNotAcceptable } from './not-acceptable.js'
|
|
8
16
|
import { sendNotAllowed } from './not-allowed.js'
|
|
9
17
|
import { sendNotFound } from './not-found.js'
|
|
10
18
|
import { sendNotImplemented } from './not-implemented.js'
|
|
11
19
|
import { sendNotModified } from './not-modified.js'
|
|
20
|
+
import { sendPartialContent } from './partial-content.js'
|
|
21
|
+
import { sendPermanentRedirect } from './permanent-redirect.js'
|
|
12
22
|
import { sendPreconditionFailed } from './precondition-failed.js'
|
|
13
23
|
import { sendPreflight } from './preflight.js'
|
|
24
|
+
import { sendRangeNotSatisfiable } from './range-not-satisfiable.js'
|
|
25
|
+
import { sendSeeOther } from './see-other.js'
|
|
14
26
|
import { sendSSE } from './sse.js'
|
|
27
|
+
import { sendTemporaryRedirect } from './temporary-redirect.js'
|
|
15
28
|
import { sendTimeout } from './timeout.js'
|
|
16
29
|
import { sendTooManyRequests } from './too-many-requests.js'
|
|
17
30
|
import { sendTrace } from './trace.js'
|
|
@@ -22,19 +35,32 @@ import { sendUnsupportedMediaType } from './unsupported-media.js'
|
|
|
22
35
|
|
|
23
36
|
export const Response = {
|
|
24
37
|
accepted: sendAccepted,
|
|
38
|
+
bytes: sendBytes,
|
|
25
39
|
conflict: sendConflict,
|
|
40
|
+
contentTooLarge: sendContentTooLarge,
|
|
26
41
|
created: sendCreated,
|
|
27
42
|
error: sendError,
|
|
43
|
+
forbidden: sendForbidden,
|
|
44
|
+
gone: sendGone,
|
|
45
|
+
imATeapot: sendImATeapot,
|
|
46
|
+
insufficientStorage: sendInsufficientStorage,
|
|
28
47
|
json: sendJSON_Encoded,
|
|
48
|
+
movedPermanently: sendMovedPermanently,
|
|
49
|
+
multipleChoices: sendMultipleChoices,
|
|
29
50
|
noContent: sendNoContent,
|
|
30
51
|
notAcceptable: sendNotAcceptable,
|
|
31
52
|
notAllowed: sendNotAllowed,
|
|
32
53
|
notFound: sendNotFound,
|
|
33
54
|
notImplemented: sendNotImplemented,
|
|
34
55
|
notModified: sendNotModified,
|
|
56
|
+
partialContent: sendPartialContent,
|
|
57
|
+
permanentRedirect: sendPermanentRedirect,
|
|
35
58
|
preconditionFailed: sendPreconditionFailed,
|
|
36
59
|
preflight: sendPreflight,
|
|
60
|
+
rangeNotSatisfiable: sendRangeNotSatisfiable,
|
|
61
|
+
seeOther: sendSeeOther,
|
|
37
62
|
sse: sendSSE,
|
|
63
|
+
temporaryRedirect: sendTemporaryRedirect,
|
|
38
64
|
timeout: sendTimeout,
|
|
39
65
|
tooManyRequests: sendTooManyRequests,
|
|
40
66
|
trace: sendTrace,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
HTTP2_HEADER_LOCATION
|
|
9
|
+
} = http2.constants
|
|
10
|
+
|
|
11
|
+
const { HTTP_STATUS_SEE_OTHER } = http2.constants
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ServerHttp2Stream} stream
|
|
15
|
+
* @param {URL} location
|
|
16
|
+
* @param {Metadata} meta
|
|
17
|
+
*/
|
|
18
|
+
export function sendSeeOther(stream, location, meta) {
|
|
19
|
+
send(stream, HTTP_STATUS_SEE_OTHER, {
|
|
20
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
21
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
22
|
+
}
|
|
@@ -1,12 +1,131 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { Readable } from 'node:stream'
|
|
3
|
+
import { ReadableStream } from 'node:stream/web'
|
|
4
|
+
import {
|
|
5
|
+
brotliCompressSync,
|
|
6
|
+
deflateSync,
|
|
7
|
+
gzipSync,
|
|
8
|
+
zstdCompressSync
|
|
9
|
+
} from 'node:zlib'
|
|
10
|
+
|
|
1
11
|
import {
|
|
2
12
|
coreHeaders,
|
|
3
13
|
customHeaders,
|
|
4
14
|
performanceHeaders
|
|
5
15
|
} from './header-util.js'
|
|
16
|
+
import { ContentRange } from '../content-range.js'
|
|
17
|
+
import { CacheControl } from '../cache-control.js'
|
|
18
|
+
import { Conditional } from '../conditional.js'
|
|
19
|
+
import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
|
|
20
|
+
import { CHARSET_UTF8 } from '../content-type.js'
|
|
6
21
|
|
|
7
22
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
8
23
|
/** @import { IncomingHttpHeaders } from 'node:http2' */
|
|
24
|
+
/** @import { InputType } from 'node:zlib' */
|
|
9
25
|
/** @import { Metadata } from './defs.js' */
|
|
26
|
+
/** @import { EtagItem } from '../conditional.js' */
|
|
27
|
+
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
28
|
+
/** @import { ContentRangeDirective } from '../content-range.js' */
|
|
29
|
+
|
|
30
|
+
/** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
HTTP2_HEADER_CONTENT_ENCODING,
|
|
34
|
+
HTTP2_HEADER_VARY,
|
|
35
|
+
HTTP2_HEADER_CACHE_CONTROL,
|
|
36
|
+
HTTP2_HEADER_ETAG,
|
|
37
|
+
HTTP2_HEADER_AGE,
|
|
38
|
+
HTTP2_HEADER_ACCEPT_RANGES,
|
|
39
|
+
HTTP2_HEADER_CONTENT_RANGE,
|
|
40
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
41
|
+
HTTP2_HEADER_ACCEPT,
|
|
42
|
+
HTTP2_HEADER_ACCEPT_ENCODING,
|
|
43
|
+
HTTP2_HEADER_RANGE
|
|
44
|
+
} = http2.constants
|
|
45
|
+
|
|
46
|
+
/** @typedef { (data: InputType) => Buffer } EncoderFun */
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, EncoderFun>} */
|
|
49
|
+
export const ENCODER_MAP = new Map([
|
|
50
|
+
[ 'br', data => brotliCompressSync(data) ],
|
|
51
|
+
[ 'gzip', data => gzipSync(data) ],
|
|
52
|
+
[ 'deflate', data => deflateSync(data) ],
|
|
53
|
+
[ 'zstd', data => zstdCompressSync(data) ]
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {ServerHttp2Stream} stream
|
|
58
|
+
* @param {number} status
|
|
59
|
+
* @param {string|undefined} contentType
|
|
60
|
+
* @param {SendBody} body
|
|
61
|
+
* @param {string|undefined} encoding
|
|
62
|
+
* @param {EtagItem|undefined} etag
|
|
63
|
+
* @param {number|undefined} age
|
|
64
|
+
* @param {CacheControlOptions|undefined} cacheControl
|
|
65
|
+
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
66
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
67
|
+
* @param {Metadata} meta
|
|
68
|
+
*/
|
|
69
|
+
export function send_encoded(stream, status, contentType, body, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
70
|
+
const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
|
|
71
|
+
|
|
72
|
+
const useIdentity = encoding === 'identity'
|
|
73
|
+
const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
|
|
74
|
+
const hasEncoder = encoder !== undefined
|
|
75
|
+
const actualEncoding = hasEncoder ? encoding : undefined
|
|
76
|
+
|
|
77
|
+
const encodeStart = performance.now()
|
|
78
|
+
const encodedData = (hasEncoder && !useIdentity) ? encoder(obj) : obj
|
|
79
|
+
const encodeEnd = performance.now()
|
|
80
|
+
|
|
81
|
+
meta.performance.push(
|
|
82
|
+
{ name: 'encode', duration: encodeEnd - encodeStart }
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {ServerHttp2Stream} stream
|
|
91
|
+
* @param {number} status
|
|
92
|
+
* @param {string|undefined} contentType
|
|
93
|
+
* @param {SendBody|undefined} obj
|
|
94
|
+
* @param {ContentRangeDirective|undefined} range
|
|
95
|
+
* @param {number|undefined} contentLength
|
|
96
|
+
* @param {string|undefined} encoding
|
|
97
|
+
* @param {EtagItem|undefined} etag
|
|
98
|
+
* @param {number|undefined} age
|
|
99
|
+
* @param {CacheControlOptions|undefined} cacheControl
|
|
100
|
+
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
101
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
102
|
+
* @param {Metadata} meta
|
|
103
|
+
*/
|
|
104
|
+
export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
105
|
+
const contentLen = Number.isInteger(contentLength) ? `${contentLength}` : undefined
|
|
106
|
+
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
107
|
+
|
|
108
|
+
const exposedHeaders = [ ]
|
|
109
|
+
if(age !== undefined) { exposedHeaders.push(HTTP2_HEADER_AGE) }
|
|
110
|
+
if(acceptRanges !== undefined) { exposedHeaders.push(HTTP2_HEADER_ACCEPT_RANGES) }
|
|
111
|
+
if(range !== undefined) { exposedHeaders.push(HTTP2_HEADER_CONTENT_RANGE) }
|
|
112
|
+
if(supportsQuery) { exposedHeaders.push(HTTP_HEADER_ACCEPT_QUERY) }
|
|
113
|
+
|
|
114
|
+
const varyHeaders = [ HTTP2_HEADER_ACCEPT, HTTP2_HEADER_ACCEPT_ENCODING ]
|
|
115
|
+
if(range !== undefined) { varyHeaders.push(HTTP2_HEADER_RANGE) } // todo: very on range is true even if not returning a content range (multipart/byteranges)
|
|
116
|
+
|
|
117
|
+
send(stream, status, {
|
|
118
|
+
[HTTP2_HEADER_CONTENT_ENCODING]: encoding,
|
|
119
|
+
[HTTP2_HEADER_VARY]: varyHeaders.join(','),
|
|
120
|
+
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
121
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
122
|
+
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined,
|
|
123
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
|
|
124
|
+
[HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
|
|
125
|
+
[HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
|
|
126
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
127
|
+
}, exposedHeaders, contentType, obj, meta)
|
|
128
|
+
}
|
|
10
129
|
|
|
11
130
|
/**
|
|
12
131
|
* @param {ServerHttp2Stream} stream
|
|
@@ -14,7 +133,7 @@ import {
|
|
|
14
133
|
* @param {IncomingHttpHeaders} headers
|
|
15
134
|
* @param {Array<string>} exposedHeaders
|
|
16
135
|
* @param {string|undefined} contentType
|
|
17
|
-
* @param {
|
|
136
|
+
* @param {SendBody|undefined} body
|
|
18
137
|
* @param {Metadata} meta
|
|
19
138
|
*/
|
|
20
139
|
export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
|
|
@@ -22,9 +141,10 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
22
141
|
if(status === 401) { console.warn(status, body) }
|
|
23
142
|
if(status === 404) { console.warn(status, body) }
|
|
24
143
|
if(status >= 500) { console.warn(status, body) }
|
|
144
|
+
// console.log('SEND', status, body?.byteLength)
|
|
25
145
|
|
|
26
|
-
if(stream === undefined) { return }
|
|
27
|
-
if(stream.closed) { return }
|
|
146
|
+
if(stream === undefined) { console.log('send - end stream undef'); return }
|
|
147
|
+
if(stream.closed) { console.log('send - end closed'); return }
|
|
28
148
|
|
|
29
149
|
if(!stream.headersSent) {
|
|
30
150
|
const custom = customHeaders(meta)
|
|
@@ -39,6 +159,11 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
39
159
|
}
|
|
40
160
|
|
|
41
161
|
if(stream.writable && body !== undefined) {
|
|
162
|
+
if(body instanceof ReadableStream) {
|
|
163
|
+
Readable.fromWeb(body).pipe(stream)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
42
167
|
stream.end(body)
|
|
43
168
|
return
|
|
44
169
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
import { send } from './send-util.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
5
|
+
/** @import { Metadata } from './defs.js' */
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
HTTP2_HEADER_LOCATION
|
|
9
|
+
} = http2.constants
|
|
10
|
+
|
|
11
|
+
const { HTTP_STATUS_TEMPORARY_REDIRECT } = http2.constants
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ServerHttp2Stream} stream
|
|
15
|
+
* @param {URL} location
|
|
16
|
+
* @param {Metadata} meta
|
|
17
|
+
*/
|
|
18
|
+
export function sendTemporaryRedirect(stream, location, meta) {
|
|
19
|
+
send(stream, HTTP_STATUS_TEMPORARY_REDIRECT, {
|
|
20
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
21
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
22
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
import { HTTP_HEADER_ACCEPT_POST } from './defs.js'
|
|
2
|
+
import { HTTP_HEADER_ACCEPT_POST, HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
|
|
3
3
|
import { send } from './send-util.js'
|
|
4
4
|
|
|
5
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
@@ -10,12 +10,17 @@ const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
|
|
|
10
10
|
/**
|
|
11
11
|
* @param {ServerHttp2Stream} stream
|
|
12
12
|
* @param {Array<string>|string} acceptableMediaType
|
|
13
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
13
14
|
* @param {Metadata} meta
|
|
14
15
|
*/
|
|
15
|
-
export function sendUnsupportedMediaType(stream, acceptableMediaType, meta) {
|
|
16
|
+
export function sendUnsupportedMediaType(stream, acceptableMediaType, supportedQueryTypes, meta) {
|
|
16
17
|
const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
|
|
17
18
|
|
|
19
|
+
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
20
|
+
const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
|
|
21
|
+
|
|
18
22
|
send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
|
|
19
|
-
[HTTP_HEADER_ACCEPT_POST]: acceptable.join(',')
|
|
20
|
-
|
|
23
|
+
[HTTP_HEADER_ACCEPT_POST]: acceptable.join(','),
|
|
24
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
25
|
+
}, exposedHeaders, undefined, undefined, meta)
|
|
21
26
|
}
|