@johntalton/http-util 4.1.1 → 5.0.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/README.md +232 -21
- package/package.json +1 -1
- package/src/accept-encoding.js +1 -1
- package/src/accept-language.js +1 -1
- package/src/accept-util.js +1 -1
- package/src/accept.js +2 -5
- package/src/body.js +13 -15
- package/src/cache-control.js +6 -1
- package/src/clear-site-data.js +59 -0
- package/src/conditional.js +37 -42
- package/src/content-disposition.js +12 -7
- package/src/content-range.js +59 -0
- package/src/content-type.js +17 -14
- package/src/forwarded.js +1 -1
- package/src/index.js +6 -1
- package/src/multipart.js +81 -33
- package/src/preference.js +205 -0
- package/src/range.js +154 -0
- package/src/response/accepted.js +1 -0
- package/src/response/bytes.js +27 -0
- package/src/response/conflict.js +1 -0
- package/src/response/content-too-large.js +16 -0
- package/src/response/created.js +2 -1
- package/src/response/defs.js +15 -1
- package/src/response/error.js +1 -0
- package/src/response/forbidden.js +17 -0
- package/src/response/gone.js +16 -0
- package/src/response/header-util.js +2 -1
- package/src/response/im-a-teapot.js +16 -0
- package/src/response/index.js +17 -0
- package/src/response/insufficient-storage.js +16 -0
- package/src/response/json.js +6 -53
- package/src/response/moved-permanently.js +23 -0
- package/src/response/multiple-choices.js +21 -0
- package/src/response/no-content.js +2 -1
- package/src/response/not-acceptable.js +2 -1
- package/src/response/not-allowed.js +1 -0
- package/src/response/not-found.js +1 -0
- package/src/response/not-implemented.js +1 -0
- package/src/response/not-modified.js +3 -2
- package/src/response/partial-content.js +71 -0
- package/src/response/permanent-redirect.js +23 -0
- package/src/response/precondition-failed.js +1 -0
- package/src/response/preflight.js +21 -5
- package/src/response/range-not-satisfiable.js +28 -0
- package/src/response/response.js +26 -0
- package/src/response/see-other.js +23 -0
- package/src/response/send-util.js +137 -6
- package/src/response/sse.js +4 -3
- package/src/response/temporary-redirect.js +23 -0
- package/src/response/timeout.js +1 -0
- package/src/response/too-many-requests.js +1 -0
- package/src/response/trace.js +8 -4
- package/src/response/unauthorized.js +1 -0
- package/src/response/unavailable.js +1 -0
- package/src/response/unprocessable.js +1 -0
- package/src/response/unsupported-media.js +15 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
|
|
3
|
+
import { send } from './send-util.js'
|
|
4
|
+
|
|
5
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
+
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
HTTP2_HEADER_LOCATION
|
|
10
|
+
} = http2.constants
|
|
11
|
+
|
|
12
|
+
const { HTTP_STATUS_SEE_OTHER } = http2.constants
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {ServerHttp2Stream} stream
|
|
16
|
+
* @param {URL} location
|
|
17
|
+
* @param {Metadata} meta
|
|
18
|
+
*/
|
|
19
|
+
export function sendSeeOther(stream, location, meta) {
|
|
20
|
+
send(stream, HTTP_STATUS_SEE_OTHER, {
|
|
21
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
22
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
23
|
+
}
|
|
@@ -1,3 +1,18 @@
|
|
|
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
|
+
|
|
11
|
+
import { CacheControl } from '../cache-control.js'
|
|
12
|
+
import { Conditional } from '../conditional.js'
|
|
13
|
+
import { ContentRange } from '../content-range.js'
|
|
14
|
+
import { CHARSET_UTF8 } from '../content-type.js'
|
|
15
|
+
import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
|
|
1
16
|
import {
|
|
2
17
|
coreHeaders,
|
|
3
18
|
customHeaders,
|
|
@@ -6,7 +21,117 @@ import {
|
|
|
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
|
+
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
34
|
+
HTTP_STATUS_NOT_FOUND,
|
|
35
|
+
HTTP_STATUS_UNAUTHORIZED
|
|
36
|
+
} = http2.constants
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
HTTP2_HEADER_CONTENT_ENCODING,
|
|
40
|
+
HTTP2_HEADER_VARY,
|
|
41
|
+
HTTP2_HEADER_CACHE_CONTROL,
|
|
42
|
+
HTTP2_HEADER_ETAG,
|
|
43
|
+
HTTP2_HEADER_AGE,
|
|
44
|
+
HTTP2_HEADER_ACCEPT_RANGES,
|
|
45
|
+
HTTP2_HEADER_CONTENT_RANGE,
|
|
46
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
47
|
+
HTTP2_HEADER_ACCEPT,
|
|
48
|
+
HTTP2_HEADER_ACCEPT_ENCODING,
|
|
49
|
+
HTTP2_HEADER_RANGE
|
|
50
|
+
} = http2.constants
|
|
51
|
+
|
|
52
|
+
/** @typedef { (data: InputType) => Buffer } EncoderFun */
|
|
53
|
+
|
|
54
|
+
/** @type {Map<string, EncoderFun>} */
|
|
55
|
+
export const ENCODER_MAP = new Map([
|
|
56
|
+
[ 'br', data => brotliCompressSync(data) ],
|
|
57
|
+
[ 'gzip', data => gzipSync(data) ],
|
|
58
|
+
[ 'deflate', data => deflateSync(data) ],
|
|
59
|
+
[ 'zstd', data => zstdCompressSync(data) ]
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {ServerHttp2Stream} stream
|
|
64
|
+
* @param {number} status
|
|
65
|
+
* @param {string|undefined} contentType
|
|
66
|
+
* @param {SendBody} body
|
|
67
|
+
* @param {string|undefined} encoding
|
|
68
|
+
* @param {EtagItem|undefined} etag
|
|
69
|
+
* @param {number|undefined} age
|
|
70
|
+
* @param {CacheControlOptions|undefined} cacheControl
|
|
71
|
+
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
72
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
73
|
+
* @param {Metadata} meta
|
|
74
|
+
*/
|
|
75
|
+
export function send_encoded(stream, status, contentType, body, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
76
|
+
const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
|
|
77
|
+
|
|
78
|
+
const useIdentity = encoding === 'identity'
|
|
79
|
+
const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
|
|
80
|
+
const hasEncoder = encoder !== undefined
|
|
81
|
+
const actualEncoding = hasEncoder ? encoding : undefined
|
|
82
|
+
|
|
83
|
+
const encodeStart = performance.now()
|
|
84
|
+
const encodedData = (hasEncoder && !useIdentity) ? encoder(obj) : obj
|
|
85
|
+
const encodeEnd = performance.now()
|
|
86
|
+
|
|
87
|
+
meta.performance.push(
|
|
88
|
+
{ name: 'encode', duration: encodeEnd - encodeStart }
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {ServerHttp2Stream} stream
|
|
97
|
+
* @param {number} status
|
|
98
|
+
* @param {string|undefined} contentType
|
|
99
|
+
* @param {SendBody|undefined} obj
|
|
100
|
+
* @param {ContentRangeDirective|undefined} range
|
|
101
|
+
* @param {number|undefined} contentLength
|
|
102
|
+
* @param {string|undefined} encoding
|
|
103
|
+
* @param {EtagItem|undefined} etag
|
|
104
|
+
* @param {number|undefined} age
|
|
105
|
+
* @param {CacheControlOptions|undefined} cacheControl
|
|
106
|
+
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
107
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
108
|
+
* @param {Metadata} meta
|
|
109
|
+
*/
|
|
110
|
+
export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
111
|
+
const contentLen = Number.isInteger(contentLength) ? `${contentLength}` : undefined
|
|
112
|
+
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
113
|
+
|
|
114
|
+
const exposedHeaders = [ ]
|
|
115
|
+
if(age !== undefined) { exposedHeaders.push(HTTP2_HEADER_AGE) }
|
|
116
|
+
if(acceptRanges !== undefined) { exposedHeaders.push(HTTP2_HEADER_ACCEPT_RANGES) }
|
|
117
|
+
if(range !== undefined) { exposedHeaders.push(HTTP2_HEADER_CONTENT_RANGE) }
|
|
118
|
+
if(supportsQuery) { exposedHeaders.push(HTTP_HEADER_ACCEPT_QUERY) }
|
|
119
|
+
|
|
120
|
+
const varyHeaders = [ HTTP2_HEADER_ACCEPT, HTTP2_HEADER_ACCEPT_ENCODING ]
|
|
121
|
+
if(range !== undefined) { varyHeaders.push(HTTP2_HEADER_RANGE) } // todo: very on range is true even if not returning a content range (multipart/byteranges)
|
|
122
|
+
|
|
123
|
+
send(stream, status, {
|
|
124
|
+
[HTTP2_HEADER_CONTENT_ENCODING]: encoding,
|
|
125
|
+
[HTTP2_HEADER_VARY]: varyHeaders.join(','),
|
|
126
|
+
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
127
|
+
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
128
|
+
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined,
|
|
129
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
|
|
130
|
+
[HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
|
|
131
|
+
[HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
|
|
132
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
133
|
+
}, exposedHeaders, contentType, obj, meta)
|
|
134
|
+
}
|
|
10
135
|
|
|
11
136
|
/**
|
|
12
137
|
* @param {ServerHttp2Stream} stream
|
|
@@ -14,17 +139,18 @@ import {
|
|
|
14
139
|
* @param {IncomingHttpHeaders} headers
|
|
15
140
|
* @param {Array<string>} exposedHeaders
|
|
16
141
|
* @param {string|undefined} contentType
|
|
17
|
-
* @param {
|
|
142
|
+
* @param {SendBody|undefined} body
|
|
18
143
|
* @param {Metadata} meta
|
|
19
144
|
*/
|
|
20
145
|
export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
|
|
21
146
|
// if(status >= 400) { console.warn(status, body) }
|
|
22
|
-
if(status ===
|
|
23
|
-
if(status ===
|
|
24
|
-
if(status >=
|
|
147
|
+
if(status === HTTP_STATUS_UNAUTHORIZED) { console.warn(status, body) }
|
|
148
|
+
if(status === HTTP_STATUS_NOT_FOUND) { console.warn(status, body) }
|
|
149
|
+
if(status >= HTTP_STATUS_INTERNAL_SERVER_ERROR) { console.warn(status, body) }
|
|
150
|
+
// console.log('SEND', status, body?.byteLength)
|
|
25
151
|
|
|
26
|
-
if(stream === undefined) { return }
|
|
27
|
-
if(stream.closed) { return }
|
|
152
|
+
if(stream === undefined) { console.log('send - end stream undef'); return }
|
|
153
|
+
if(stream.closed) { console.log('send - end closed'); return }
|
|
28
154
|
|
|
29
155
|
if(!stream.headersSent) {
|
|
30
156
|
const custom = customHeaders(meta)
|
|
@@ -39,6 +165,11 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
39
165
|
}
|
|
40
166
|
|
|
41
167
|
if(stream.writable && body !== undefined) {
|
|
168
|
+
if(body instanceof ReadableStream) {
|
|
169
|
+
Readable.fromWeb(body).pipe(stream)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
42
173
|
stream.end(body)
|
|
43
174
|
return
|
|
44
175
|
}
|
package/src/response/sse.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
|
-
SSE_MIME,
|
|
4
|
-
SSE_INACTIVE_STATUS_CODE,
|
|
5
|
-
SSE_BOM,
|
|
6
4
|
ENDING,
|
|
5
|
+
SSE_BOM,
|
|
6
|
+
SSE_INACTIVE_STATUS_CODE,
|
|
7
|
+
SSE_MIME,
|
|
7
8
|
} from '@johntalton/sse-util'
|
|
8
9
|
import { coreHeaders, performanceHeaders } from './header-util.js'
|
|
9
10
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
|
|
3
|
+
import { send } from './send-util.js'
|
|
4
|
+
|
|
5
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
+
/** @import { Metadata } from './defs.js' */
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
HTTP2_HEADER_LOCATION
|
|
10
|
+
} = http2.constants
|
|
11
|
+
|
|
12
|
+
const { HTTP_STATUS_TEMPORARY_REDIRECT } = http2.constants
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {ServerHttp2Stream} stream
|
|
16
|
+
* @param {URL} location
|
|
17
|
+
* @param {Metadata} meta
|
|
18
|
+
*/
|
|
19
|
+
export function sendTemporaryRedirect(stream, location, meta) {
|
|
20
|
+
send(stream, HTTP_STATUS_TEMPORARY_REDIRECT, {
|
|
21
|
+
[HTTP2_HEADER_LOCATION]: location.href
|
|
22
|
+
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
23
|
+
}
|
package/src/response/timeout.js
CHANGED
package/src/response/trace.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
+
|
|
2
3
|
import { CONTENT_TYPE_MESSAGE_HTTP } from '../content-type.js'
|
|
3
4
|
import { send } from './send-util.js'
|
|
4
5
|
|
|
@@ -8,6 +9,9 @@ import { send } from './send-util.js'
|
|
|
8
9
|
|
|
9
10
|
const { HTTP_STATUS_OK } = http2.constants
|
|
10
11
|
|
|
12
|
+
const LINE_ENDING = '\n'
|
|
13
|
+
const PSEUDO_HEADER_PREFIX = ':'
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* @param {ServerHttp2Stream} stream
|
|
13
17
|
* @param {string} method
|
|
@@ -27,13 +31,13 @@ export function sendTrace(stream, method, url, headers, meta) {
|
|
|
27
31
|
const reconstructed = [
|
|
28
32
|
`${method} ${url.pathname}${url.search} ${version}`,
|
|
29
33
|
Object.entries(headers)
|
|
30
|
-
.filter(([ key ]) => !key.startsWith(
|
|
34
|
+
.filter(([ key ]) => !key.startsWith(PSEUDO_HEADER_PREFIX))
|
|
31
35
|
.filter(([ key ]) => !FILTER_KEYS.includes(key))
|
|
32
36
|
.map(([ key, value ]) => `${key}: ${value}`)
|
|
33
|
-
.join(
|
|
34
|
-
|
|
37
|
+
.join(LINE_ENDING),
|
|
38
|
+
LINE_ENDING
|
|
35
39
|
]
|
|
36
|
-
.join(
|
|
40
|
+
.join(LINE_ENDING)
|
|
37
41
|
|
|
38
42
|
send(stream, HTTP_STATUS_OK, {}, [], CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
|
|
39
43
|
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { HTTP_HEADER_ACCEPT_PATCH, HTTP_HEADER_ACCEPT_POST, HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
|
|
3
4
|
import { send } from './send-util.js'
|
|
4
5
|
|
|
5
6
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
7
|
/** @import { Metadata } from './defs.js' */
|
|
7
8
|
|
|
9
|
+
const { HTTP2_METHOD_POST, HTTP2_METHOD_PATCH } = http2.constants
|
|
10
|
+
|
|
8
11
|
const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* @param {ServerHttp2Stream} stream
|
|
12
15
|
* @param {Array<string>|string} acceptableMediaType
|
|
16
|
+
* @param {Array<string>|undefined} supportedQueryTypes
|
|
13
17
|
* @param {Metadata} meta
|
|
14
18
|
*/
|
|
15
|
-
export function sendUnsupportedMediaType(stream, acceptableMediaType, meta) {
|
|
19
|
+
export function sendUnsupportedMediaType(stream, acceptableMediaType, supportedQueryTypes, meta) {
|
|
20
|
+
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
21
|
+
const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
|
|
22
|
+
|
|
23
|
+
const method = HTTP2_METHOD_POST // todo pass in as parameter or split acceptable to post and patch types
|
|
16
24
|
const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
|
|
25
|
+
const acceptHeader = (method === HTTP2_METHOD_POST) ? HTTP_HEADER_ACCEPT_POST : HTTP_HEADER_ACCEPT_PATCH
|
|
26
|
+
const acceptValue = ((method === HTTP2_METHOD_POST) || (method === HTTP2_METHOD_PATCH)) ? acceptable.join(',') : undefined
|
|
17
27
|
|
|
18
28
|
send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
|
|
19
|
-
[
|
|
20
|
-
|
|
29
|
+
[acceptHeader]: acceptValue,
|
|
30
|
+
[HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
|
|
31
|
+
}, exposedHeaders, undefined, undefined, meta)
|
|
21
32
|
}
|