mockaton 11.2.6 → 11.3.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.
@@ -3,8 +3,7 @@ import { randomUUID } from 'node:crypto'
3
3
 
4
4
  import { extFor } from './utils/mime.js'
5
5
  import { write, isFile } from './utils/fs.js'
6
- import { readBody, BodyReaderError } from './utils/http-request.js'
7
- import { sendUnprocessable, sendBadGateway } from './utils/http-response.js'
6
+ import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
8
7
 
9
8
  import { config } from './config.js'
10
9
  import { makeMockFilename } from './Filename.js'
@@ -23,9 +22,9 @@ export async function proxy(req, response, delay) {
23
22
  }
24
23
  catch (error) { // TESTME
25
24
  if (error instanceof BodyReaderError)
26
- sendUnprocessable(response, error.name)
25
+ response.unprocessable(error.name)
27
26
  else
28
- sendBadGateway(response, error)
27
+ response.badGateway(error)
29
28
  return
30
29
  }
31
30
 
@@ -4,7 +4,6 @@ import { readFileSync } from 'node:fs'
4
4
  import { isFile } from './utils/fs.js'
5
5
  import { logger } from './utils/logger.js'
6
6
  import { mimeFor } from './utils/mime.js'
7
- import { sendMockNotFound, sendPartialContent } from './utils/http-response.js'
8
7
 
9
8
  import { brokerByRoute } from './staticCollection.js'
10
9
  import { config, calcDelay } from './config.js'
@@ -16,19 +15,19 @@ export async function dispatchStatic(req, response) {
16
15
 
17
16
  setTimeout(async () => {
18
17
  if (!broker || broker.status === 404) {
19
- sendMockNotFound(response)
18
+ response.mockNotFound()
20
19
  return
21
20
  }
22
21
 
23
22
  const file = join(config.staticDir, broker.route)
24
23
  if (!isFile(file)) {
25
- sendMockNotFound(response)
24
+ response.mockNotFound()
26
25
  return
27
26
  }
28
27
 
29
28
  logger.accessMock(req.url, 'static200')
30
29
  if (req.headers.range)
31
- await sendPartialContent(response, req.headers.range, file)
30
+ await response.partialContent(req.headers.range, file)
32
31
  else {
33
32
  response.setHeader('Content-Type', mimeFor(file))
34
33
  response.end(readFileSync(file))
@@ -8,7 +8,6 @@ import {
8
8
  } from './ApiConstants.js'
9
9
 
10
10
  import { config } from './config.js'
11
- import { sendJSON } from './utils/http-response.js'
12
11
  import { isFile, isDirectory } from './utils/fs.js'
13
12
 
14
13
  import * as staticCollection from './staticCollection.js'
@@ -101,12 +100,12 @@ export function longPollClientSyncVersion(req, response) {
101
100
  const clientVersion = req.headers[HEADER_SYNC_VERSION]
102
101
  if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
103
102
  // e.g., tab was hidden while new mocks were added or removed
104
- sendJSON(response, uiSyncVersion.version)
103
+ response.json(uiSyncVersion.version)
105
104
  return
106
105
  }
107
106
  function onARR() {
108
107
  uiSyncVersion.unsubscribe(onARR)
109
- sendJSON(response, uiSyncVersion.version)
108
+ response.json(uiSyncVersion.version)
110
109
  }
111
110
  response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onARR)
112
111
  req.on('error', () => {
@@ -1,7 +1,6 @@
1
1
  import { join } from 'node:path'
2
2
  import { EventEmitter } from 'node:events'
3
3
  import { watch, readdirSync } from 'node:fs'
4
- import { sendJSON, sendNotFound } from './utils/http-response.js'
5
4
  import { LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
6
5
 
7
6
 
@@ -30,17 +29,17 @@ export function watchDevSPA() {
30
29
  /** Realtime notify Dev UI changes */
31
30
  export function longPollDevClientHotReload(req, response) {
32
31
  if (!DEV) {
33
- sendNotFound(response)
32
+ response.notFound()
34
33
  return
35
34
  }
36
35
 
37
36
  function onDevChange(file) {
38
37
  devClientWatcher.unsubscribe(onDevChange)
39
- sendJSON(response, file)
38
+ response.json(file)
40
39
  }
41
40
  response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
42
41
  devClientWatcher.unsubscribe(onDevChange)
43
- sendJSON(response, '')
42
+ response.json('')
44
43
  })
45
44
  req.on('error', () => {
46
45
  devClientWatcher.unsubscribe(onDevChange)
@@ -4,7 +4,7 @@ import { logger } from './utils/logger.js'
4
4
  import { isDirectory } from './utils/fs.js'
5
5
  import { openInBrowser } from './utils/openInBrowser.js'
6
6
  import { optional, is, validate } from './utils/validate.js'
7
- import { SUPPORTED_METHODS } from './utils/http-request.js'
7
+ import { SUPPORTED_METHODS } from './utils/HttpIncomingMessage.js'
8
8
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
9
9
 
10
10
  import { jsToJsonPlugin } from './MockDispatcher.js'
@@ -1,4 +1,4 @@
1
- import { METHODS } from 'node:http'
1
+ import http, { METHODS } from 'node:http'
2
2
 
3
3
 
4
4
  export const SUPPORTED_METHODS = METHODS
@@ -12,6 +12,12 @@ export class BodyReaderError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ export class IncomingMessage extends http.IncomingMessage {
16
+ json() {
17
+ return readBody(this, JSON.parse)
18
+ }
19
+ }
20
+
15
21
  export const parseJSON = req => readBody(req, JSON.parse)
16
22
 
17
23
  export function readBody(req, parser = a => a) {
@@ -0,0 +1,117 @@
1
+ import http from 'node:http'
2
+ import fs, { readFileSync } from 'node:fs'
3
+
4
+ import { logger } from './logger.js'
5
+ import { mimeFor } from './mime.js'
6
+ import { HEADER_502 } from '../ApiConstants.js'
7
+
8
+
9
+ export class ServerResponse extends http.ServerResponse {
10
+ setHeaderList(headers) {
11
+ for (let i = 0; i < headers.length; i += 2)
12
+ this.setHeader(headers[i], headers[i + 1])
13
+ }
14
+
15
+ ok() {
16
+ logger.access(this)
17
+ this.end()
18
+ }
19
+
20
+ html(html, csp) {
21
+ logger.access(this)
22
+ this.setHeader('Content-Type', mimeFor('html'))
23
+ this.setHeader('Content-Security-Policy', csp)
24
+ this.end(html)
25
+ }
26
+
27
+ json(payload) {
28
+ logger.access(this)
29
+ this.setHeader('Content-Type', 'application/json')
30
+ this.end(JSON.stringify(payload))
31
+ }
32
+
33
+ file(file) {
34
+ logger.access(this)
35
+ this.setHeader('Content-Type', mimeFor(file))
36
+ this.end(readFileSync(file, 'utf8'))
37
+ }
38
+
39
+ noContent() {
40
+ this.statusCode = 204
41
+ logger.access(this)
42
+ this.end()
43
+ }
44
+
45
+
46
+ badRequest() {
47
+ this.statusCode = 400
48
+ logger.access(this)
49
+ this.end()
50
+ }
51
+
52
+ notFound() {
53
+ this.statusCode = 404
54
+ logger.access(this)
55
+ this.end()
56
+ }
57
+
58
+ mockNotFound() {
59
+ this.statusCode = 404
60
+ logger.accessMock(this.req.url, '404')
61
+ this.end()
62
+ }
63
+
64
+ uriTooLong() {
65
+ this.statusCode = 414
66
+ logger.access(this)
67
+ this.end()
68
+ }
69
+
70
+ unprocessable(error) {
71
+ logger.access(this, error)
72
+ this.statusCode = 422
73
+ this.end(error)
74
+ }
75
+
76
+
77
+ internalServerError(error) {
78
+ logger.error(500, this.req.url, error?.message || error, error?.stack || '')
79
+ this.statusCode = 500
80
+ this.end()
81
+ }
82
+
83
+ badGateway(error) {
84
+ logger.warn('Fallback Proxy Error:', error.cause.message)
85
+ this.statusCode = 502
86
+ this.setHeader(HEADER_502, 1)
87
+ this.end()
88
+ }
89
+
90
+
91
+ async partialContent(range, file) {
92
+ const { size } = await fs.promises.lstat(file)
93
+ let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
94
+ if (isNaN(end)) end = size - 1
95
+ if (isNaN(start)) start = size - end
96
+
97
+ if (start < 0 || start > end || start >= size || end >= size) {
98
+ this.statusCode = 416 // Range Not Satisfiable
99
+ this.setHeader('Content-Range', `bytes */${size}`)
100
+ this.end()
101
+ }
102
+ else {
103
+ this.statusCode = 206 // Partial Content
104
+ this.setHeader('Accept-Ranges', 'bytes')
105
+ this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
106
+ this.setHeader('Content-Type', mimeFor(file))
107
+ const reader = fs.createReadStream(file, { start, end })
108
+ const response = this
109
+ reader.on('open', function () {
110
+ this.pipe(response)
111
+ })
112
+ reader.on('error', error => {
113
+ this.internalServerError(error)
114
+ })
115
+ }
116
+ }
117
+ }
@@ -1,4 +1,4 @@
1
- import { methodIsSupported } from './http-request.js'
1
+ import { methodIsSupported } from './HttpIncomingMessage.js'
2
2
 
3
3
  /* https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-processing-model */
4
4
 
@@ -0,0 +1,225 @@
1
+ import { equal } from 'node:assert/strict'
2
+ import { promisify } from 'node:util'
3
+ import { createServer } from 'node:http'
4
+ import { describe, test, after } from 'node:test'
5
+ import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
6
+
7
+
8
+ function headerIs(response, header, value) {
9
+ equal(response.headers.get(header), value)
10
+ }
11
+
12
+ const FooDotCom = 'http://foo.com'
13
+ const AllowedDotCom = 'http://allowed.com'
14
+ const NotAllowedDotCom = 'http://not-allowed.com'
15
+
16
+ await describe('CORS', async () => {
17
+ let corsConfig = {}
18
+
19
+ const server = createServer((req, response) => {
20
+ setCorsHeaders(req, response, corsConfig)
21
+ if (isPreflight(req)) {
22
+ response.statusCode = 204
23
+ response.end()
24
+ }
25
+ else
26
+ response.end('NON_PREFLIGHT')
27
+ })
28
+ await promisify(server.listen).bind(server, 0, '127.0.0.1')()
29
+ after(() => {
30
+ server.close()
31
+ })
32
+ function preflight(headers, method = 'OPTIONS') {
33
+ const { address, port } = server.address()
34
+ return fetch(`http://${address}:${port}/`, { method, headers })
35
+ }
36
+ function request(headers, method) {
37
+ const { address, port } = server.address()
38
+ return fetch(`http://${address}:${port}/`, { method, headers })
39
+ }
40
+
41
+ await describe('Identifies Preflight Requests', async () => {
42
+ const requiredRequestHeaders = {
43
+ [CH.Origin]: 'http://localhost:9999',
44
+ [CH.AcRequestMethod]: 'POST'
45
+ }
46
+
47
+ await test('Ignores non-OPTIONS requests', async () => {
48
+ const res = await request(requiredRequestHeaders, 'POST')
49
+ equal(await res.text(), 'NON_PREFLIGHT')
50
+ })
51
+
52
+ await test(`Ignores non-parseable req ${CH.Origin} header`, async () => {
53
+ const headers = {
54
+ ...requiredRequestHeaders,
55
+ [CH.Origin]: 'non-url'
56
+ }
57
+ const res = await preflight(headers)
58
+ equal(await res.text(), 'NON_PREFLIGHT')
59
+ })
60
+
61
+ await test(`Ignores missing method in ${CH.AcRequestMethod} header`, async () => {
62
+ const headers = { ...requiredRequestHeaders }
63
+ delete headers[CH.AcRequestMethod]
64
+ const res = await preflight(headers)
65
+ equal(await res.text(), 'NON_PREFLIGHT')
66
+ })
67
+
68
+ await test(`Ignores non-standard method in ${CH.AcRequestMethod} header`, async () => {
69
+ const headers = {
70
+ ...requiredRequestHeaders,
71
+ [CH.AcRequestMethod]: 'NON_STANDARD'
72
+ }
73
+ const res = await preflight(headers)
74
+ equal(await res.text(), 'NON_PREFLIGHT')
75
+ })
76
+
77
+ await test('204 valid preflights', async () => {
78
+ const res = await preflight(requiredRequestHeaders)
79
+ equal(res.status, 204)
80
+ })
81
+ })
82
+
83
+ await describe('Preflight Response Headers', async () => {
84
+ await test('no origins allowed', async () => {
85
+ corsConfig = {
86
+ corsOrigins: [],
87
+ corsMethods: ['GET']
88
+ }
89
+ const p = await preflight({
90
+ [CH.Origin]: FooDotCom,
91
+ [CH.AcRequestMethod]: 'GET'
92
+ })
93
+ headerIs(p, CH.AcAllowOrigin, null)
94
+ headerIs(p, CH.AcAllowMethods, null)
95
+ headerIs(p, CH.AcAllowCredentials, null)
96
+ headerIs(p, CH.AcAllowHeaders, null)
97
+ headerIs(p, CH.AcMaxAge, null)
98
+ })
99
+
100
+ await test('not in allowed origins', async () => {
101
+ corsConfig = {
102
+ corsOrigins: [AllowedDotCom],
103
+ corsMethods: ['GET']
104
+ }
105
+ const p = await preflight({
106
+ [CH.Origin]: NotAllowedDotCom,
107
+ [CH.AcRequestMethod]: 'GET'
108
+ })
109
+ headerIs(p, CH.AcAllowOrigin, null)
110
+ headerIs(p, CH.AcAllowMethods, null)
111
+ headerIs(p, CH.AcAllowCredentials, null)
112
+ headerIs(p, CH.AcAllowHeaders, null)
113
+ })
114
+
115
+ await test('origin and method match', async () => {
116
+ corsConfig = {
117
+ corsOrigins: [AllowedDotCom],
118
+ corsMethods: ['GET']
119
+ }
120
+ const p = await preflight({
121
+ [CH.Origin]: AllowedDotCom,
122
+ [CH.AcRequestMethod]: 'GET'
123
+ })
124
+ headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
125
+ headerIs(p, CH.AcAllowMethods, 'GET')
126
+ headerIs(p, CH.AcAllowCredentials, null)
127
+ headerIs(p, CH.AcAllowHeaders, null)
128
+ })
129
+
130
+ await test('origin matches from multiple', async () => {
131
+ corsConfig = {
132
+ corsOrigins: [AllowedDotCom, FooDotCom],
133
+ corsMethods: ['GET']
134
+ }
135
+ const p = await preflight({
136
+ [CH.Origin]: AllowedDotCom,
137
+ [CH.AcRequestMethod]: 'GET'
138
+ })
139
+ headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
140
+ headerIs(p, CH.AcAllowMethods, 'GET')
141
+ headerIs(p, CH.AcAllowCredentials, null)
142
+ headerIs(p, CH.AcAllowHeaders, null)
143
+ })
144
+
145
+ await test('wildcard origin', async () => {
146
+ corsConfig = {
147
+ corsOrigins: ['*'],
148
+ corsMethods: ['GET']
149
+ }
150
+ const p = await preflight({
151
+ [CH.Origin]: FooDotCom,
152
+ [CH.AcRequestMethod]: 'GET'
153
+ })
154
+ headerIs(p, CH.AcAllowOrigin, FooDotCom)
155
+ headerIs(p, CH.AcAllowMethods, 'GET')
156
+ headerIs(p, CH.AcAllowCredentials, null)
157
+ headerIs(p, CH.AcAllowHeaders, null)
158
+ })
159
+
160
+ await test(`wildcard and credentials`, async () => {
161
+ corsConfig = {
162
+ corsOrigins: ['*'],
163
+ corsMethods: ['GET'],
164
+ corsCredentials: true
165
+ }
166
+ const p = await preflight({
167
+ [CH.Origin]: FooDotCom,
168
+ [CH.AcRequestMethod]: 'GET'
169
+ })
170
+ headerIs(p, CH.AcAllowOrigin, FooDotCom)
171
+ headerIs(p, CH.AcAllowMethods, 'GET')
172
+ headerIs(p, CH.AcAllowCredentials, 'true')
173
+ headerIs(p, CH.AcAllowHeaders, null)
174
+ })
175
+
176
+ await test(`wildcard, credentials, and headers`, async () => {
177
+ corsConfig = {
178
+ corsOrigins: ['*'],
179
+ corsMethods: ['GET'],
180
+ corsCredentials: true,
181
+ corsHeaders: ['content-type', 'my-header']
182
+ }
183
+ const p = await preflight({
184
+ [CH.Origin]: FooDotCom,
185
+ [CH.AcRequestMethod]: 'GET'
186
+ })
187
+ headerIs(p, CH.AcAllowOrigin, FooDotCom)
188
+ headerIs(p, CH.AcAllowMethods, 'GET')
189
+ headerIs(p, CH.AcAllowCredentials, 'true')
190
+ headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
191
+ })
192
+ })
193
+
194
+ await describe('Non-Preflight (Actual Response) Headers', async () => {
195
+ await test('no origins allowed', async () => {
196
+ corsConfig = {
197
+ corsOrigins: [],
198
+ corsMethods: ['GET']
199
+ }
200
+ const p = await request({
201
+ [CH.Origin]: NotAllowedDotCom
202
+ })
203
+ equal(p.status, 200)
204
+ headerIs(p, CH.AcAllowOrigin, null)
205
+ headerIs(p, CH.AcAllowCredentials, null)
206
+ headerIs(p, CH.AcExposeHeaders, null)
207
+ })
208
+
209
+ await test('origin allowed', async () => {
210
+ corsConfig = {
211
+ corsOrigins: [AllowedDotCom],
212
+ corsMethods: ['GET'],
213
+ corsCredentials: true,
214
+ corsExposedHeaders: ['x-h1', 'x-h2']
215
+ }
216
+ const p = await request({
217
+ [CH.Origin]: AllowedDotCom
218
+ })
219
+ equal(p.status, 200)
220
+ headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
221
+ headerIs(p, CH.AcAllowCredentials, 'true')
222
+ headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
223
+ })
224
+ })
225
+ })
@@ -1,4 +1,4 @@
1
- import { decode, reControlAndDelChars } from './http-request.js'
1
+ import { decode, reControlAndDelChars } from './HttpIncomingMessage.js'
2
2
 
3
3
 
4
4
  export const logger = new class {
@@ -0,0 +1,24 @@
1
+ import { test } from 'node:test'
2
+ import { equal } from 'node:assert/strict'
3
+ import { parseMime, extFor, mimeFor } from './mime.js'
4
+
5
+
6
+ test('parseMime', () => [
7
+ 'text/html',
8
+ 'TEXT/html',
9
+ 'text/html; charset=utf-8'
10
+ ].map(input =>
11
+ equal(parseMime(input), 'text/html')))
12
+
13
+ test('extFor', () => [
14
+ 'text/html',
15
+ 'Text/html',
16
+ 'text/Html; charset=UTF-16'
17
+ ].map(input =>
18
+ equal(extFor(input), 'html')))
19
+
20
+ test('mimeFor', () => [
21
+ 'file.html',
22
+ 'file.HTmL'
23
+ ].map(input =>
24
+ equal(mimeFor(input), 'text/html')))
@@ -0,0 +1,47 @@
1
+ import { describe, test } from 'node:test'
2
+ import { doesNotThrow, throws } from 'node:assert/strict'
3
+ import { validate, is, optional } from './validate.js'
4
+
5
+
6
+ describe('validate', () => {
7
+ describe('optional', () => {
8
+ test('accepts undefined', () =>
9
+ doesNotThrow(() =>
10
+ validate({}, { field: optional(Number.isInteger) })))
11
+
12
+ test('accepts falsy value regardless of type', () =>
13
+ doesNotThrow(() =>
14
+ validate({ field: 0 }, { field: optional(Array.isArray) })))
15
+
16
+ test('accepts when tester func returns truthy', () =>
17
+ doesNotThrow(() =>
18
+ validate({ field: [] }, { field: optional(Array.isArray) })))
19
+
20
+ test('rejects when tester func returns falsy', () =>
21
+ throws(() =>
22
+ validate({ field: 1 }, { field: optional(Array.isArray) }),
23
+ /field=1 is invalid/))
24
+ })
25
+
26
+ describe('is', () => {
27
+ test('rejects mismatched type', () =>
28
+ throws(() =>
29
+ validate({ field: 1 }, { field: is(String) }),
30
+ /field=1 is invalid/))
31
+
32
+ test('accepts matched type', () =>
33
+ doesNotThrow(() =>
34
+ validate({ field: '' }, { field: is(String) })))
35
+ })
36
+
37
+ describe('custom tester func', () => {
38
+ test('rejects mismatched type', () =>
39
+ throws(() =>
40
+ validate({ field: 0.1 }, { field: Number.isInteger }),
41
+ /field=0.1 is invalid/))
42
+
43
+ test('accepts matched type', () =>
44
+ doesNotThrow(() =>
45
+ validate({ field: 1 }, { field: Number.isInteger })))
46
+ })
47
+ })
@@ -1,113 +0,0 @@
1
- import fs, { readFileSync } from 'node:fs'
2
-
3
- import { logger } from './logger.js'
4
- import { mimeFor } from './mime.js'
5
- import { HEADER_502 } from '../ApiConstants.js'
6
-
7
-
8
- export function setHeaders(response, headers) {
9
- for (let i = 0; i < headers.length; i += 2)
10
- response.setHeader(headers[i], headers[i + 1])
11
- }
12
-
13
- export function sendOK(response) {
14
- logger.access(response)
15
- response.end()
16
- }
17
-
18
- export function sendHTML(response, html, csp) {
19
- logger.access(response)
20
- response.setHeader('Content-Type', mimeFor('html'))
21
- response.setHeader('Content-Security-Policy', csp)
22
- response.end(html)
23
- }
24
-
25
- export function sendJSON(response, payload) {
26
- logger.access(response)
27
- response.setHeader('Content-Type', 'application/json')
28
- response.end(JSON.stringify(payload))
29
- }
30
-
31
- export function sendFile(response, file) {
32
- logger.access(response)
33
- response.setHeader('Content-Type', mimeFor(file))
34
- response.end(readFileSync(file, 'utf8'))
35
- }
36
-
37
- export function sendNoContent(response) {
38
- response.statusCode = 204
39
- logger.access(response)
40
- response.end()
41
- }
42
-
43
-
44
- export function sendBadRequest(response) {
45
- response.statusCode = 400
46
- logger.access(response)
47
- response.end()
48
- }
49
-
50
- export function sendNotFound(response) {
51
- response.statusCode = 404
52
- logger.access(response)
53
- response.end()
54
- }
55
-
56
- export function sendMockNotFound(response) {
57
- response.statusCode = 404
58
- logger.accessMock(response.req.url, '404')
59
- response.end()
60
- }
61
-
62
- export function sendTooLongURI(response) {
63
- response.statusCode = 414
64
- logger.access(response)
65
- response.end()
66
- }
67
-
68
- export function sendUnprocessable(response, error) {
69
- logger.access(response, error)
70
- response.statusCode = 422
71
- response.end(error)
72
- }
73
-
74
-
75
- export function sendInternalServerError(response, error) {
76
- logger.error(500, response.req.url, error?.message || error, error?.stack || '')
77
- response.statusCode = 500
78
- response.end()
79
- }
80
-
81
- export function sendBadGateway(response, error) {
82
- logger.warn('Fallback Proxy Error:', error.cause.message)
83
- response.statusCode = 502
84
- response.setHeader(HEADER_502, 1)
85
- response.end()
86
- }
87
-
88
-
89
- export async function sendPartialContent(response, range, file) {
90
- const { size } = await fs.promises.lstat(file)
91
- let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
92
- if (isNaN(end)) end = size - 1
93
- if (isNaN(start)) start = size - end
94
-
95
- if (start < 0 || start > end || start >= size || end >= size) {
96
- response.statusCode = 416 // Range Not Satisfiable
97
- response.setHeader('Content-Range', `bytes */${size}`)
98
- response.end()
99
- }
100
- else {
101
- response.statusCode = 206 // Partial Content
102
- response.setHeader('Accept-Ranges', 'bytes')
103
- response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
104
- response.setHeader('Content-Type', mimeFor(file))
105
- const reader = fs.createReadStream(file, { start, end })
106
- reader.on('open', function () {
107
- this.pipe(response)
108
- })
109
- reader.on('error', function (error) {
110
- sendInternalServerError(response, error)
111
- })
112
- }
113
- }