@johntalton/http-util 5.1.6 → 6.1.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.
Files changed (84) hide show
  1. package/README.md +54 -7
  2. package/package.json +26 -6
  3. package/src/body.js +8 -8
  4. package/src/{response/defs.js → defs.js} +35 -1
  5. package/src/{accept-encoding.js → headers/accept-encoding.js} +11 -14
  6. package/src/{accept-language.js → headers/accept-language.js} +14 -9
  7. package/src/headers/accept.js +86 -0
  8. package/src/{cache-control.js → headers/cache-control.js} +0 -6
  9. package/src/{clear-site-data.js → headers/clear-site-data.js} +4 -10
  10. package/src/headers/client-hints.js +88 -0
  11. package/src/{conditional.js → headers/conditional.js} +190 -117
  12. package/src/headers/content-disposition.js +44 -0
  13. package/src/{content-range.js → headers/content-range.js} +1 -18
  14. package/src/headers/content-type.js +101 -0
  15. package/src/{forwarded.js → headers/forwarded.js} +8 -56
  16. package/src/{index.js → headers/index.js} +4 -2
  17. package/src/headers/link.js +34 -0
  18. package/src/{multipart.js → headers/multipart.js} +22 -13
  19. package/src/{preference.js → headers/preference.js} +3 -58
  20. package/src/{range.js → headers/range.js} +4 -32
  21. package/src/{rate-limit.js → headers/rate-limit.js} +6 -1
  22. package/src/{server-timing.js → headers/server-timing.js} +3 -16
  23. package/src/headers/strict-transport-security.js +39 -0
  24. package/src/{accept-util.js → headers/util/accept-util.js} +8 -14
  25. package/src/headers/util/index.js +7 -0
  26. package/src/headers/util/kvp.js +79 -0
  27. package/src/headers/util/mime.js +77 -0
  28. package/src/headers/util/whitespace.js +8 -0
  29. package/src/{www-authenticate.js → headers/www-authenticate.js} +1 -1
  30. package/src/response/{accepted.js → 2xx/accepted.js} +2 -2
  31. package/src/response/2xx/bytes.js +62 -0
  32. package/src/response/2xx/created.js +49 -0
  33. package/src/response/2xx/json.js +60 -0
  34. package/src/response/2xx/no-content.js +45 -0
  35. package/src/response/2xx/partial-content.js +101 -0
  36. package/src/response/{preflight.js → 2xx/preflight.js} +29 -10
  37. package/src/response/{sse.js → 2xx/sse.js} +2 -2
  38. package/src/response/{trace.js → 2xx/trace.js} +3 -3
  39. package/src/response/3xx/found.js +23 -0
  40. package/src/response/{moved-permanently.js → 3xx/moved-permanently.js} +2 -2
  41. package/src/response/{multiple-choices.js → 3xx/multiple-choices.js} +2 -3
  42. package/src/response/3xx/not-modified.js +59 -0
  43. package/src/response/{permanent-redirect.js → 3xx/permanent-redirect.js} +2 -2
  44. package/src/response/{see-other.js → 3xx/see-other.js} +2 -2
  45. package/src/response/{temporary-redirect.js → 3xx/temporary-redirect.js} +2 -2
  46. package/src/response/4xx/bad-request.js +19 -0
  47. package/src/response/{conflict.js → 4xx/conflict.js} +2 -2
  48. package/src/response/{content-too-large.js → 4xx/content-too-large.js} +2 -2
  49. package/src/response/{forbidden.js → 4xx/forbidden.js} +3 -2
  50. package/src/response/{gone.js → 4xx/gone.js} +2 -2
  51. package/src/response/{im-a-teapot.js → 4xx/im-a-teapot.js} +2 -2
  52. package/src/response/{not-acceptable.js → 4xx/not-acceptable.js} +14 -3
  53. package/src/response/4xx/not-allowed.js +34 -0
  54. package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
  55. package/src/response/4xx/payment-required.js +17 -0
  56. package/src/response/4xx/precondition-failed.js +45 -0
  57. package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +15 -4
  58. package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
  59. package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +22 -5
  60. package/src/response/{unauthorized.js → 4xx/unauthorized.js} +5 -5
  61. package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
  62. package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +21 -4
  63. package/src/response/{error.js → 5xx/error.js} +3 -3
  64. package/src/response/{insufficient-storage.js → 5xx/insufficient-storage.js} +2 -2
  65. package/src/response/{not-implemented.js → 5xx/not-implemented.js} +4 -4
  66. package/src/response/{unavailable.js → 5xx/unavailable.js} +16 -4
  67. package/src/response/header-util.js +2 -2
  68. package/src/response/index.js +39 -35
  69. package/src/response/response.js +40 -34
  70. package/src/response/send-util.js +32 -21
  71. package/src/accept.js +0 -122
  72. package/src/content-disposition.js +0 -57
  73. package/src/content-type.js +0 -148
  74. package/src/link.js +0 -35
  75. package/src/response/bytes.js +0 -27
  76. package/src/response/created.js +0 -28
  77. package/src/response/json.js +0 -28
  78. package/src/response/no-content.js +0 -25
  79. package/src/response/not-allowed.js +0 -23
  80. package/src/response/not-modified.js +0 -35
  81. package/src/response/partial-content.js +0 -71
  82. package/src/response/precondition-failed.js +0 -16
  83. /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
  84. /package/src/{quote.js → headers/util/quote.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import http2 from 'node:http2'
2
- import { Readable } from 'node:stream'
2
+ import { pipeline, Readable } from 'node:stream'
3
3
  import { ReadableStream } from 'node:stream/web'
4
4
  import {
5
5
  brotliCompressSync,
@@ -8,11 +8,11 @@ import {
8
8
  zstdCompressSync
9
9
  } from 'node:zlib'
10
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'
11
+ import { HTTP_HEADER_ACCEPT_QUERY } from '../defs.js'
12
+ import { CacheControl } from '../headers/cache-control.js'
13
+ import { Conditional } from '../headers/conditional.js'
14
+ import { ContentRange } from '../headers/content-range.js'
15
+ import { CHARSET_UTF8 } from '../headers/content-type.js'
16
16
  import {
17
17
  coreHeaders,
18
18
  customHeaders,
@@ -22,12 +22,10 @@ import {
22
22
  /** @import { ServerHttp2Stream } from 'node:http2' */
23
23
  /** @import { OutgoingHttpHeaders } from 'node:http2' */
24
24
  /** @import { InputType } from 'node:zlib' */
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 */
25
+ /** @import { AcceptRangeUnits, Metadata, SendBody } from '../defs.js' */
26
+ /** @import { EtagItem, IMFFixDateInput } from '../headers/conditional.js' */
27
+ /** @import { CacheControlOptions } from '../headers/cache-control.js' */
28
+ /** @import { ContentRangeDirective } from '../headers/content-range.js' */
31
29
 
32
30
  const {
33
31
  HTTP_STATUS_INTERNAL_SERVER_ERROR,
@@ -40,6 +38,7 @@ const {
40
38
  HTTP2_HEADER_VARY,
41
39
  HTTP2_HEADER_CACHE_CONTROL,
42
40
  HTTP2_HEADER_ETAG,
41
+ HTTP2_HEADER_LAST_MODIFIED,
43
42
  HTTP2_HEADER_AGE,
44
43
  HTTP2_HEADER_ACCEPT_RANGES,
45
44
  HTTP2_HEADER_CONTENT_RANGE,
@@ -49,7 +48,7 @@ const {
49
48
  HTTP2_HEADER_RANGE
50
49
  } = http2.constants
51
50
 
52
- /** @typedef { (data: InputType) => Buffer } EncoderFun */
51
+ /** @typedef { (data: InputType) => Buffer<ArrayBuffer> } EncoderFun */
53
52
 
54
53
  /** @type {Map<string, EncoderFun>} */
55
54
  export const ENCODER_MAP = new Map([
@@ -66,17 +65,18 @@ export const ENCODER_MAP = new Map([
66
65
  * @param {SendBody} body
67
66
  * @param {string|undefined} encoding
68
67
  * @param {EtagItem|undefined} etag
68
+ * @param {IMFFixDateInput|string|undefined} lastModified
69
69
  * @param {number|undefined} age
70
70
  * @param {CacheControlOptions|undefined} cacheControl
71
- * @param {'bytes'|'none'|undefined} acceptRanges
71
+ * @param {AcceptRangeUnits|undefined} acceptRanges
72
72
  * @param {Array<string>|undefined} supportedQueryTypes
73
73
  * @param {Metadata} meta
74
74
  */
75
- export function send_encoded(stream, status, contentType, body, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
75
+ export function send_encoded(stream, status, contentType, body, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
76
76
  const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
77
77
 
78
78
  const useIdentity = encoding === 'identity'
79
- const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
79
+ const encoder = encoding === undefined ? undefined : ENCODER_MAP.get(encoding)
80
80
  const hasEncoder = encoder !== undefined
81
81
  const actualEncoding = hasEncoder ? encoding : undefined
82
82
 
@@ -88,7 +88,7 @@ export function send_encoded(stream, status, contentType, body, encoding, etag,
88
88
  { name: 'encode', duration: encodeEnd - encodeStart }
89
89
  )
90
90
 
91
- send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
91
+ send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
92
92
  }
93
93
 
94
94
 
@@ -101,13 +101,14 @@ export function send_encoded(stream, status, contentType, body, encoding, etag,
101
101
  * @param {number|undefined} contentLength
102
102
  * @param {string|undefined} encoding
103
103
  * @param {EtagItem|undefined} etag
104
+ * @param {IMFFixDateInput|string|undefined} lastModified
104
105
  * @param {number|undefined} age
105
106
  * @param {CacheControlOptions|undefined} cacheControl
106
- * @param {'bytes'|'none'|undefined} acceptRanges
107
+ * @param {AcceptRangeUnits|undefined} acceptRanges
107
108
  * @param {Array<string>|undefined} supportedQueryTypes
108
109
  * @param {Metadata} meta
109
110
  */
110
- export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
111
+ export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
111
112
  const contentLen = Number.isInteger(contentLength) ? `${contentLength}` : undefined
112
113
  const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
113
114
 
@@ -125,7 +126,8 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
125
126
  [HTTP2_HEADER_VARY]: varyHeaders.join(','),
126
127
  [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
127
128
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
128
- [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined,
129
+ [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified),
130
+ [HTTP2_HEADER_AGE]: age === undefined ? undefined : `${age}`,
129
131
  [HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
130
132
  [HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
131
133
  [HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
@@ -166,7 +168,16 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
166
168
 
167
169
  if(stream.writable && body !== undefined) {
168
170
  if(body instanceof ReadableStream) {
169
- Readable.fromWeb(body).pipe(stream)
171
+ const signal = undefined // AbortSignal.timeout(1000)
172
+ pipeline(
173
+ Readable.fromWeb(body, { signal }),
174
+ stream,
175
+ err => {
176
+ if(err !== null) {
177
+ console.warn('pipeline error')
178
+ }
179
+ })
180
+
170
181
  return
171
182
  }
172
183
 
package/src/accept.js DELETED
@@ -1,122 +0,0 @@
1
- import { parseAcceptStyleHeader } from './accept-util.js'
2
-
3
- /**
4
- * @import { AcceptStyleItem } from './accept-util.js'
5
- */
6
-
7
- /**
8
- * @typedef {Object} AcceptExtensionItem
9
- * @property {string} mimetype
10
- * @property {string} type
11
- * @property {string} subtype
12
- */
13
-
14
- /**
15
- * @typedef {AcceptStyleItem & AcceptExtensionItem} AcceptItem
16
- */
17
-
18
- export const ACCEPT_SEPARATOR = { SUBTYPE: '/' }
19
- export const ACCEPT_ANY = '*'
20
-
21
- export const WELL_KNOWN = new Map([
22
- [ '*/*', [ { name: '*/*', quality: 1 } ] ],
23
- [ 'application/json', [ { name: 'application/json', quality: 1 } ] ]
24
- ])
25
-
26
- export class Accept {
27
- /**
28
- * @param {string|undefined} acceptHeader
29
- * @returns {Array<AcceptItem>}
30
- */
31
- static parse(acceptHeader) {
32
- return parseAcceptStyleHeader(acceptHeader, WELL_KNOWN)
33
- .map(({ name, quality, parameters }) => {
34
- const [ type, subtype ] = name
35
- .split(ACCEPT_SEPARATOR.SUBTYPE)
36
- .map(t => t.trim())
37
-
38
- return {
39
- mimetype: `${type}${ACCEPT_SEPARATOR.SUBTYPE}${subtype ?? ACCEPT_ANY}`,
40
- name, type, subtype,
41
- quality,
42
- parameters
43
- }
44
- })
45
- .sort((entryA, entryB) => {
46
- if(entryA.quality === entryB.quality) {
47
- // prefer things with less ANY
48
- const specificityA = (entryA.type === ACCEPT_ANY ? 1 : 0) + (entryA.subtype === ACCEPT_ANY ? 1 : 0)
49
- const specificityB = (entryB.type === ACCEPT_ANY ? 1 : 0) + (entryB.subtype === ACCEPT_ANY ? 1 : 0)
50
- return specificityA - specificityB
51
- }
52
-
53
- // B - A descending order
54
- const qualityB = entryB.quality ?? 0
55
- const qualityA = entryA.quality ?? 0
56
- return qualityB - qualityA
57
- })
58
- }
59
-
60
- /**
61
- * @param {string|undefined} acceptHeader
62
- * @param {Array<string>} supportedTypes
63
- */
64
- static select(acceptHeader, supportedTypes) {
65
- const accepts = Accept.parse(acceptHeader)
66
- return Accept.selectFrom(accepts, supportedTypes)
67
- }
68
-
69
- /**
70
- * @param {Array<AcceptItem>} accepts
71
- * @param {Array<string>} supportedTypes
72
- */
73
- static selectFrom(accepts, supportedTypes) {
74
- const bests = accepts.map(accept => {
75
- const { type, subtype, quality } = accept
76
- const st = supportedTypes.filter(supportedType => {
77
- const [ stType, stSubtype ] = supportedType.split(ACCEPT_SEPARATOR.SUBTYPE)
78
- return ((stType === type || type === ACCEPT_ANY) && (stSubtype === subtype || subtype === ACCEPT_ANY))
79
- })
80
-
81
- return {
82
- supportedTypes: st,
83
- quality
84
- }
85
- })
86
- .filter(best => best.supportedTypes.length > 0)
87
-
88
- if(bests.length === 0) { return undefined }
89
- const [ first ] = bests
90
- if(first === undefined) { return undefined }
91
- const [ firstSt ] = first.supportedTypes
92
- return firstSt
93
- }
94
- }
95
-
96
- // console.log(Accept.parse('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7'))
97
- // console.log(Accept.select('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7', [ 'application/json', 'text/plain' ]))
98
-
99
- // const tests = [
100
- // undefined,
101
- // '',
102
- // ' ',
103
- // ' fake',
104
- // ' application/json',
105
- // ' application/xml,',
106
- // ' ,application/xml ,,',
107
- // ' audio/*; q=0.2, audio/basic',
108
- // ' text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8',
109
- // ' text/*;q=0.3, text/plain;q=0.7, text/plain;format=flowed,\ntext/plain;format=fixed;q=0.4, */*;q=0.5',
110
-
111
- // ' */*, foo/bar, foo/*, biz/bang, */*;q=.2, quix/quak;q=.1',
112
- // 'foo / bar ; q = .5'
113
- // ]
114
-
115
- // tests.forEach(test => {
116
- // const result = Accept.parse(test)
117
- // console.log('=============================')
118
- // console.log({ test })
119
- // console.log('---')
120
- // console.log(result)
121
- // })
122
-
@@ -1,57 +0,0 @@
1
- /**
2
- * @typedef {Object} Disposition
3
- * @property {string} disposition
4
- * @property {Map<string, string|undefined>} parameters
5
- * @property {string|undefined} [name]
6
- * @property {string|undefined} [filename]
7
- */
8
-
9
- export const DISPOSITION_SEPARATOR = {
10
- PARAMETER: ';',
11
- KVP: '='
12
- }
13
-
14
- export const DISPOSITION_PARAM_NAME = 'name'
15
- export const DISPOSITION_PARAM_FILENAME = 'filename'
16
-
17
- /**
18
- * @param {string|undefined} contentDispositionHeader
19
- * @returns {Disposition|undefined}
20
- */
21
- export function parseContentDisposition(contentDispositionHeader) {
22
- if(contentDispositionHeader === undefined) { return undefined }
23
-
24
- const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
25
- if(disposition === undefined) { return undefined }
26
- const parameters = new Map(parameterSet.map(parameter => {
27
- const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
28
- if(key === undefined) { return undefined }
29
- return { key, value }
30
- })
31
- .filter(item => item !== undefined)
32
- .map(({ key, value }) => ([ key, value ])))
33
-
34
-
35
- const name = parameters.get(DISPOSITION_PARAM_NAME)
36
- const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
37
-
38
- return {
39
- disposition,
40
- parameters,
41
- name, filename
42
- }
43
- }
44
-
45
- // console.log(parseContentDisposition())
46
- // // console.log(parseContentDisposition(null))
47
- // console.log(parseContentDisposition(''))
48
- // console.log(parseContentDisposition('form-data'))
49
- // console.log(parseContentDisposition(' form-data ; name'))
50
- // console.log(parseContentDisposition('form-data; name="key"'))
51
-
52
- // console.log(parseContentDisposition('inline'))
53
- // console.log(parseContentDisposition('attachment'))
54
- // console.log(parseContentDisposition('attachment; filename="file name.jpg"'))
55
- // console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
56
- // console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
57
- // console.log(parseContentDisposition('form-data;title*=us-ascii\'en-us\'This%20is%20%2A%2A%2Afun%2A%2A%2A'))
@@ -1,148 +0,0 @@
1
-
2
- export const MIME_TYPE_JSON = 'application/json'
3
- export const MIME_TYPE_TEXT = 'text/plain'
4
- export const MIME_TYPE_EVENT_STREAM = 'text/event-stream'
5
- export const MIME_TYPE_XML = 'application/xml'
6
- export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
7
- export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
8
- export const MIME_TYPE_MULTIPART_RANGE = 'multipart/byteranges'
9
- export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
10
- export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
11
-
12
-
13
- export const KNOWN_CONTENT_TYPES = [
14
- 'application', 'audio', 'image', 'message',
15
- 'multipart','text', 'video'
16
- ]
17
-
18
- export const TYPE_X_TOKEN_PREFIX = 'X-'
19
-
20
- export const SPECIAL_CHARS = [
21
- // special
22
- '(', ')', '<', '>',
23
- '@', ',', ';', ':',
24
- '\\', '"', '/', '[',
25
- ']', '?', '.', '=',
26
- // space
27
- ' ', '\u000B', '\u000C',
28
- // control
29
- '\n', '\r', '\t'
30
- ]
31
-
32
- export const WHITESPACE_REGEX = /\s/
33
-
34
- /**
35
- * @param {string} c
36
- */
37
- export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
38
-
39
- /**
40
- * @param {string|undefined} value
41
- */
42
- export function hasSpecialChar(value) {
43
- if(value === undefined) { return false }
44
- for(const special of SPECIAL_CHARS) {
45
- if(value.includes(special)) { return true}
46
- }
47
-
48
- return false
49
- }
50
-
51
- /**
52
- * @typedef {Object} ContentType
53
- * @property {string} mimetype
54
- * @property {string} mimetypeRaw
55
- * @property {string} type
56
- * @property {string} subtype
57
- * @property {string} [charset]
58
- * @property {Map<string, string>} parameters
59
- */
60
-
61
- export const CONTENT_TYPE_SEPARATOR = {
62
- SUBTYPE: '/',
63
- PARAMETER: ';',
64
- KVP: '='
65
- }
66
-
67
- export const CHARSET_UTF8 = 'utf8'
68
- export const CHARSET = 'charset'
69
- export const PARAMETER_CHARSET_UTF8 = `${CHARSET}${CONTENT_TYPE_SEPARATOR.KVP}${CHARSET_UTF8}`
70
- export const CONTENT_TYPE_JSON = `${MIME_TYPE_JSON}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
71
- export const CONTENT_TYPE_TEXT = `${MIME_TYPE_TEXT}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
72
- export const CONTENT_TYPE_MESSAGE_HTTP = `${MIME_TYPE_MESSAGE_HTTP}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
73
-
74
- /** @type {ContentType} */
75
- export const WELL_KNOWN_JSON = {
76
- mimetype: 'application/json',
77
- mimetypeRaw: 'application/json',
78
- type: 'application',
79
- subtype: 'json',
80
- charset: 'utf8',
81
- parameters: new Map()
82
- }
83
-
84
- export const WELL_KNOWN_CONTENT_TYPES = new Map([
85
- [ 'application/json', WELL_KNOWN_JSON ],
86
- [ 'application/json;charset=utf8', WELL_KNOWN_JSON ]
87
- ])
88
-
89
-
90
- /**
91
- * @param {string|undefined} contentTypeHeader
92
- * @returns {ContentType|undefined}
93
- */
94
- export function parseContentType(contentTypeHeader) {
95
- if(contentTypeHeader === undefined) { return undefined }
96
- if(contentTypeHeader === null) { return undefined }
97
-
98
- const wellKnown = WELL_KNOWN_CONTENT_TYPES.get(contentTypeHeader)
99
- if(wellKnown !== undefined) { return wellKnown }
100
-
101
- const [ mimetypeRaw, ...parameterSet ] = contentTypeHeader.split(CONTENT_TYPE_SEPARATOR.PARAMETER)
102
- if(mimetypeRaw === undefined) { return undefined }
103
- if(mimetypeRaw === '') { return undefined }
104
-
105
- const [ typeRaw, subtypeRaw ] = mimetypeRaw
106
- .split(CONTENT_TYPE_SEPARATOR.SUBTYPE)
107
- .map(t => t.toLowerCase())
108
-
109
- if(typeRaw === undefined) { return undefined }
110
- if(typeRaw === '') { return undefined }
111
- if(hasSpecialChar(typeRaw)) { return undefined }
112
- if(subtypeRaw === undefined) { return undefined }
113
- if(subtypeRaw === '') { return undefined }
114
- if(hasSpecialChar(subtypeRaw)) { return undefined }
115
-
116
- const type = typeRaw.trim()
117
- const subtype = subtypeRaw.trim()
118
-
119
- const parameters = new Map()
120
-
121
- for(const parameter of parameterSet) {
122
- const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
123
- if(key === undefined || key === '') { continue }
124
- if(value === undefined || value === '') { continue }
125
-
126
- const actualKey = key?.trim().toLowerCase()
127
- if(hasSpecialChar(actualKey)) { continue }
128
-
129
- const quoted = (value.at(0) === '"' && value.at(-1) === '"')
130
- const actualValue = quoted ? value.substring(1, value.length - 1) : value
131
-
132
- if(!parameters.has(actualKey)) {
133
- parameters.set(actualKey, actualValue)
134
- }
135
- }
136
-
137
- const charset = parameters.get(CHARSET)
138
-
139
- return {
140
- mimetype: `${type}${CONTENT_TYPE_SEPARATOR.SUBTYPE}${subtype}`,
141
- mimetypeRaw, type, subtype,
142
- charset,
143
- parameters
144
- }
145
- }
146
-
147
- //
148
- // console.log(parseContentType('multipart/form-data; boundary=----WebKitFormBoundaryJZy5maoMBkBMoGjt'))
package/src/link.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * @typedef {Object} LinkItem
3
- * @property {string} url
4
- * @property {string|undefined} [relation]
5
- * @property {Map<string, string>|undefined} [parameters]
6
- */
7
-
8
- export class Link {
9
- /**
10
- * @param {LinkItem} link
11
- */
12
- static *#encode(link) {
13
- const encodedUri = encodeURI(link.url)
14
-
15
- yield `<${encodedUri}>`
16
- if(link.relation !== undefined) { yield `rel="${link.relation}"` }
17
- if(link.parameters === undefined) { return }
18
- for(const [ key, value ] of link.parameters) {
19
- yield `${key}="${value}"`
20
- }
21
- }
22
-
23
-
24
- /**
25
- * @param {LinkItem} link
26
- */
27
- static encode(link) {
28
- return [ ...Link.#encode(link) ].join('; ')
29
- }
30
- }
31
-
32
- // console.log(Link.encode({ url: '/index.html', parameters: new Map([ [ 'as', 'style' ], [ 'fetchpriority', 'high' ] ]) }))
33
- // console.log(Link.encode({ url: '/index.html', relation: 'next', parameters: new Map([ [ 'fetchpriority', 'high' ] ]) }))
34
- // console.log(Link.encode({ url: '/index.html', relation: 'next' }))
35
- // console.log(Link.encode({ url: 'https://example.com/苗条', relation: 'preconnect' }))
@@ -1,27 +0,0 @@
1
- import http2 from 'node:http2'
2
-
3
- import { send_bytes } from './send-util.js'
4
-
5
- /** @import { ServerHttp2Stream } from 'node:http2' */
6
- /** @import { Metadata } from './defs.js' */
7
- /** @import { EtagItem } from '../conditional.js' */
8
- /** @import { CacheControlOptions } from '../cache-control.js' */
9
- /** @import { SendBody } from './send-util.js' */
10
-
11
- const { HTTP_STATUS_OK } = http2.constants
12
-
13
- /**
14
- * @param {ServerHttp2Stream} stream
15
- * @param {SendBody|undefined} obj
16
- * @param {string|undefined} contentType
17
- * @param {number|undefined} contentLength
18
- * @param {string|undefined} encoding
19
- * @param {EtagItem|undefined} etag
20
- * @param {number|undefined} age
21
- * @param {CacheControlOptions} cacheControl
22
- * @param {'bytes'|'none'|undefined} acceptRanges
23
- * @param {Metadata} meta
24
- */
25
- export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, age, cacheControl, acceptRanges, meta) {
26
- send_bytes(stream, HTTP_STATUS_OK, contentType, obj, undefined, contentLength, encoding, etag, age, cacheControl, acceptRanges, undefined, meta)
27
- }
@@ -1,28 +0,0 @@
1
- import http2 from 'node:http2'
2
-
3
- import { Conditional } from '../conditional.js'
4
- import { send } from './send-util.js'
5
-
6
- /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from './defs.js' */
8
- /** @import { EtagItem } from '../conditional.js' */
9
-
10
- const {
11
- HTTP2_HEADER_LOCATION,
12
- HTTP2_HEADER_ETAG
13
- } = http2.constants
14
-
15
- const { HTTP_STATUS_CREATED } = http2.constants
16
-
17
- /**
18
- * @param {ServerHttp2Stream} stream
19
- * @param {URL} location
20
- * @param {EtagItem|undefined} etag
21
- * @param {Metadata} meta
22
- */
23
- export function sendCreated(stream, location, etag, meta) {
24
- send(stream, HTTP_STATUS_CREATED, {
25
- [HTTP2_HEADER_LOCATION]: location.href,
26
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
27
- }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
28
- }
@@ -1,28 +0,0 @@
1
- import http2 from 'node:http2'
2
-
3
- import { CONTENT_TYPE_JSON } from '../content-type.js'
4
- import { send_encoded } from './send-util.js'
5
-
6
- /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from './defs.js' */
8
- /** @import { EtagItem } from '../conditional.js' */
9
- /** @import { CacheControlOptions } from '../cache-control.js' */
10
-
11
- const { HTTP_STATUS_OK } = http2.constants
12
-
13
- /**
14
- * @param {ServerHttp2Stream} stream
15
- * @param {Object} obj
16
- * @param {string|undefined} encoding
17
- * @param {EtagItem|undefined} etag
18
- * @param {number|undefined} age
19
- * @param {CacheControlOptions} cacheControl
20
- * @param {Array<string>|undefined} supportedQueryTypes
21
- * @param {Metadata} meta
22
- */
23
- export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
24
- if(stream.closed) { return }
25
-
26
- const json = JSON.stringify(obj)
27
- send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
28
- }
@@ -1,25 +0,0 @@
1
- import http2 from 'node:http2'
2
-
3
- import { Conditional } from '../conditional.js'
4
- import { send } from './send-util.js'
5
-
6
- /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from './defs.js' */
8
- /** @import { EtagItem } from '../conditional.js' */
9
-
10
- const {
11
- HTTP2_HEADER_ETAG
12
- } = http2.constants
13
-
14
- const { HTTP_STATUS_NO_CONTENT } = http2.constants
15
-
16
- /**
17
- * @param {ServerHttp2Stream} stream
18
- * @param {EtagItem|undefined} etag
19
- * @param {Metadata} meta
20
- */
21
- export function sendNoContent(stream, etag, meta) {
22
- send(stream, HTTP_STATUS_NO_CONTENT, {
23
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
24
- }, [], undefined, undefined, meta)
25
- }
@@ -1,23 +0,0 @@
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
- HTTP_STATUS_METHOD_NOT_ALLOWED
10
- } = http2.constants
11
-
12
- const { HTTP2_HEADER_ALLOW } = http2.constants
13
-
14
- /**
15
- * @param {ServerHttp2Stream} stream
16
- * @param {Array<string>} methods
17
- * @param {Metadata} meta
18
- */
19
- export function sendNotAllowed(stream, methods, meta) {
20
- send(stream, HTTP_STATUS_METHOD_NOT_ALLOWED, {
21
- [HTTP2_HEADER_ALLOW]: methods.join(',')
22
- }, [ HTTP2_HEADER_ALLOW ], undefined, undefined, meta)
23
- }
@@ -1,35 +0,0 @@
1
- import http2 from 'node:http2'
2
-
3
- import { CacheControl } from '../cache-control.js'
4
- import { Conditional } from '../conditional.js'
5
- import { send } from './send-util.js'
6
-
7
- /** @import { ServerHttp2Stream } from 'node:http2' */
8
- /** @import { Metadata } from './defs.js' */
9
- /** @import { EtagItem } from '../conditional.js' */
10
- /** @import { CacheControlOptions } from '../cache-control.js' */
11
-
12
- const {
13
- HTTP2_HEADER_AGE,
14
- HTTP2_HEADER_ETAG,
15
- HTTP2_HEADER_VARY,
16
- HTTP2_HEADER_CACHE_CONTROL
17
- } = http2.constants
18
-
19
- const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
20
-
21
- /**
22
- * @param {ServerHttp2Stream} stream
23
- * @param {EtagItem|undefined} etag
24
- * @param {number|undefined} age
25
- * @param {CacheControlOptions} cacheControl
26
- * @param {Metadata} meta
27
- */
28
- export function sendNotModified(stream, etag, age, cacheControl, meta) {
29
- send(stream, HTTP_STATUS_NOT_MODIFIED, {
30
- [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
31
- [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
32
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
33
- [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
34
- }, [ HTTP2_HEADER_AGE ], undefined, undefined, meta)
35
- }