@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.
@@ -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 {ArrayBufferLike|ArrayBufferView|string|undefined} body
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
- }, [ HTTP_HEADER_ACCEPT_POST ], undefined, undefined, meta)
23
+ [HTTP_HEADER_ACCEPT_POST]: acceptable.join(','),
24
+ [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
25
+ }, exposedHeaders, undefined, undefined, meta)
21
26
  }