@srfnstack/spliffy 1.2.5 → 1.2.6

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/src/handler.mjs CHANGED
@@ -1,256 +1,256 @@
1
- import log from './log.mjs'
2
- import { deserializeBody, serializeBody } from './content.mjs'
3
- import { invokeMiddleware } from './middleware.mjs'
4
- import { decorateResponse, decorateRequest } from './decorator.mjs'
5
- import { v4 as uuid } from 'uuid'
6
- import stream from 'stream'
7
- const { Readable } = stream
8
-
9
- /**
10
- * Execute the handler
11
- * @param url The url being requested
12
- * @param res The uws response object
13
- * @param req The uws request object
14
- * @param bodyPromise The request body promise
15
- * @param handler The handler function for the route
16
- * @param middleware The middleware that applies to this request
17
- * @param errorTransformer An errorTransformer to convert error objects into response data
18
- */
19
- const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
20
- try {
21
- bodyPromise = bodyPromise.then(bodyContent => {
22
- if (bodyContent instanceof Readable) return bodyContent
23
- if (res.writableEnded) return
24
- return deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
25
- })
26
- } catch (e) {
27
- log.error('Failed to parse request.', e)
28
- end(res, 400, handler.statusCodeOverride)
29
- return
30
- }
31
-
32
- try {
33
- const handled = await handler({ url, bodyPromise, headers: req.headers, req, res })
34
- finalizeResponse(req, res, handled, handler.statusCodeOverride)
35
- } catch (e) {
36
- const refId = uuid()
37
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
38
- endError(res, e, refId, errorTransformer)
39
- }
40
- }
41
-
42
- const endError = (res, e, refId, errorTransformer) => {
43
- if (e.body && typeof e.body !== 'string') {
44
- e.body = JSON.stringify(e.body)
45
- }
46
- if (typeof errorTransformer === 'function') {
47
- e = errorTransformer(e, refId)
48
- }
49
- res.headers['x-ref-id'] = refId
50
- const status = e.statusCode || 500
51
- if (status === 500) {
52
- log.error(e)
53
- }
54
- end(res, status, null, e.body || '')
55
- }
56
-
57
- const end = (res, defaultStatusCode, statusCodeOverride, body) => {
58
- // status set directly on res wins
59
- res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
60
- if (body instanceof Readable || res.streaming) {
61
- res.streaming = true
62
- if (body instanceof Readable) {
63
- pipeResponse(res, body)
64
- }
65
- // handler is responsible for ending the response if they are streaming
66
- } else {
67
- res.end(doSerializeBody(body, res) || '')
68
- }
69
- }
70
-
71
- const ipv6CompressRegex = /\b:?(?:0+:?){2,}/g
72
-
73
- const compressIpv6 = ip => ip && ip.includes(':') ? ip.replaceAll(ipv6CompressRegex, '::') : ip
74
-
75
- const writeAccess = function (req, res) {
76
- const start = new Date().getTime()
77
- return () => {
78
- log.access(compressIpv6(req.remoteAddress), compressIpv6(res.proxiedRemoteAddress) || '', res.statusCode, req.method, req.url, new Date().getTime() - start + 'ms')
79
- }
80
- }
81
-
82
- const finalizeResponse = (req, res, handled, statusCodeOverride) => {
83
- if (!res.finalized) {
84
- if (!handled) {
85
- // if no error was thrown, assume everything is fine. Otherwise each handler must return truthy which is un-necessary for methods that don't need to return anything
86
- end(res, 200, statusCodeOverride)
87
- } else {
88
- // if the returned object has known fields, treat it as a response object instead of the body
89
- if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
90
- if (handled.headers) {
91
- res.assignHeaders(handled.headers)
92
- }
93
- res.statusMessage = handled.statusMessage || res.statusMessage
94
- end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
95
- } else {
96
- end(res, 200, statusCodeOverride, handled)
97
- }
98
- }
99
- res.finalized = true
100
- }
101
- }
102
-
103
- const pipeResponse = (res, readStream, errorTransformer) => {
104
- readStream.on('data', res.write)
105
- .on('end', res.end)
106
- .on('error', e => {
107
- try {
108
- readStream.destroy()
109
- } finally {
110
- endError(res, e, uuid(), errorTransformer)
111
- }
112
- })
113
- }
114
-
115
- const doSerializeBody = (body, res) => {
116
- if (!body || typeof body === 'string' || body instanceof Readable) {
117
- return body
118
- }
119
- const contentType = res.getHeader('content-type')
120
- const serialized = serializeBody(body, contentType, res.acceptsDefault)
121
-
122
- if (serialized?.contentType && !contentType) {
123
- res.headers['content-type'] = serialized.contentType
124
- }
125
- return serialized?.data || ''
126
- }
127
-
128
- async function executeMiddleware (middleware, req, res, errorTransformer, refId, e) {
129
- if (!middleware) return
130
-
131
- let applicableMiddleware = middleware[req.method]
132
- if (middleware.ALL) {
133
- if (applicableMiddleware) applicableMiddleware = middleware.ALL.concat(applicableMiddleware)
134
- else applicableMiddleware = middleware.ALL
135
- }
136
-
137
- if (!applicableMiddleware || applicableMiddleware.length === 0) {
138
- return
139
- }
140
- if (e) {
141
- await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 4), req, res, e)
142
- } else {
143
- await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 3), req, res)
144
- }
145
- }
146
-
147
- const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
148
- try {
149
- let reqBody
150
- if (!handler.streamRequestBody) {
151
- let buffer
152
- reqBody = new Promise(
153
- resolve =>
154
- res.onData(async (data, isLast) => {
155
- if (isLast) {
156
- buffer = data.byteLength > 0 ? Buffer.concat([buffer, Buffer.from(data)].filter(b => b)) : buffer
157
- resolve(buffer)
158
- }
159
- buffer = Buffer.concat([buffer, Buffer.from(data)].filter(b => b))
160
- })
161
- )
162
- } else {
163
- const readable = new Readable({
164
- read: () => {
165
- }
166
- })
167
- res.onData(async (data, isLast) => {
168
- if (data.byteLength === 0 && !isLast) return
169
- // data must be copied so it isn't lost
170
- readable.push(Buffer.concat([Buffer.from(data)]))
171
- if (isLast) {
172
- readable.push(null)
173
- }
174
- })
175
- reqBody = Promise.resolve(readable)
176
- }
177
- await executeMiddleware(middleware, req, res, errorTransformer)
178
- if (!res.writableEnded && !res.ended) {
179
- await executeHandler(req.spliffyUrl, res, req, reqBody, handler, middleware, errorTransformer)
180
- }
181
- } catch (e) {
182
- const refId = uuid()
183
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
184
- if (!res.writableEnded) { endError(res, e, refId, errorTransformer) }
185
- }
186
- }
187
-
188
- export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
189
-
190
- let currentDate = new Date().toISOString()
191
- setInterval(() => { currentDate = new Date().toISOString() }, 1000)
192
-
193
- export const createHandler = (handler, middleware, pathParameters, config) => function (res, req) {
194
- try {
195
- res.cork(() => {
196
- req = decorateRequest(req, pathParameters, res, config)
197
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
198
-
199
- if (config.logAccess) {
200
- res.onEnd = writeAccess(req, res)
201
- }
202
-
203
- if (config.writeDateHeader) {
204
- res.headers.date = currentDate
205
- }
206
-
207
- handleRequest(req, res, handler, middleware, config.errorTransformer)
208
- .catch(e => {
209
- log.error('Failed handling request', e)
210
- res.statusCode = 500
211
- res.end()
212
- })
213
- })
214
- } catch (e) {
215
- log.error('Failed handling request', e)
216
- res.statusCode = 500
217
- res.end()
218
- }
219
- }
220
-
221
- export const createNotFoundHandler = config => {
222
- const handler = config.defaultRouteHandler || config.notFoundRouteHandler
223
- const params = handler?.pathParameters || []
224
- return (res, req) => {
225
- try {
226
- req = decorateRequest(req, params, res, config)
227
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
228
- if (config.logAccess) {
229
- res.onEnd = writeAccess(req, res)
230
- }
231
- if (handler && typeof handler === 'object') {
232
- if (handler.handlers && typeof handler.handlers[req.method] === 'function') {
233
- if ('statusCodeOverride' in handler) {
234
- handler.handlers[req.method].statusCodeOverride = handler.statusCodeOverride
235
- }
236
- handleRequest(req, res,
237
- handler.handlers[req.method],
238
- handler.middleware,
239
- config.errorTransformer
240
- ).catch((e) => {
241
- log.error('Unexpected exception during request handling', e)
242
- res.statusCode = 500
243
- })
244
- } else {
245
- res.statusCode = 404
246
- res.end()
247
- }
248
- } else {
249
- res.statusCode = 404
250
- res.end()
251
- }
252
- } catch (e) {
253
- log.error('Failed handling request', e)
254
- }
255
- }
256
- }
1
+ import log from './log.mjs'
2
+ import { deserializeBody, serializeBody } from './content.mjs'
3
+ import { invokeMiddleware } from './middleware.mjs'
4
+ import { decorateResponse, decorateRequest } from './decorator.mjs'
5
+ import { v4 as uuid } from 'uuid'
6
+ import stream from 'stream'
7
+ const { Readable } = stream
8
+
9
+ /**
10
+ * Execute the handler
11
+ * @param url The url being requested
12
+ * @param res The uws response object
13
+ * @param req The uws request object
14
+ * @param bodyPromise The request body promise
15
+ * @param handler The handler function for the route
16
+ * @param middleware The middleware that applies to this request
17
+ * @param errorTransformer An errorTransformer to convert error objects into response data
18
+ */
19
+ const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
20
+ try {
21
+ bodyPromise = bodyPromise.then(bodyContent => {
22
+ if (bodyContent instanceof Readable) return bodyContent
23
+ if (res.writableEnded) return
24
+ return deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
25
+ })
26
+ } catch (e) {
27
+ log.error('Failed to parse request.', e)
28
+ end(res, 400, handler.statusCodeOverride)
29
+ return
30
+ }
31
+
32
+ try {
33
+ const handled = await handler({ url, bodyPromise, headers: req.headers, req, res })
34
+ finalizeResponse(req, res, handled, handler.statusCodeOverride)
35
+ } catch (e) {
36
+ const refId = uuid()
37
+ await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
38
+ endError(res, e, refId, errorTransformer)
39
+ }
40
+ }
41
+
42
+ const endError = (res, e, refId, errorTransformer) => {
43
+ if (e.body && typeof e.body !== 'string') {
44
+ e.body = JSON.stringify(e.body)
45
+ }
46
+ if (typeof errorTransformer === 'function') {
47
+ e = errorTransformer(e, refId)
48
+ }
49
+ res.headers['x-ref-id'] = refId
50
+ const status = e.statusCode || 500
51
+ if (status === 500) {
52
+ log.error(e)
53
+ }
54
+ end(res, status, null, e.body || '')
55
+ }
56
+
57
+ const end = (res, defaultStatusCode, statusCodeOverride, body) => {
58
+ // status set directly on res wins
59
+ res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
60
+ if (body instanceof Readable || res.streaming) {
61
+ res.streaming = true
62
+ if (body instanceof Readable) {
63
+ pipeResponse(res, body)
64
+ }
65
+ // handler is responsible for ending the response if they are streaming
66
+ } else {
67
+ res.end(doSerializeBody(body, res) || '')
68
+ }
69
+ }
70
+
71
+ const ipv6CompressRegex = /\b:?(?:0+:?){2,}/g
72
+
73
+ const compressIpv6 = ip => ip && ip.includes(':') ? ip.replaceAll(ipv6CompressRegex, '::') : ip
74
+
75
+ const writeAccess = function (req, res) {
76
+ const start = new Date().getTime()
77
+ return () => {
78
+ log.access(compressIpv6(req.remoteAddress), compressIpv6(res.proxiedRemoteAddress) || '', res.statusCode, req.method, req.url, new Date().getTime() - start + 'ms')
79
+ }
80
+ }
81
+
82
+ const finalizeResponse = (req, res, handled, statusCodeOverride) => {
83
+ if (!res.finalized) {
84
+ if (!handled) {
85
+ // if no error was thrown, assume everything is fine. Otherwise each handler must return truthy which is un-necessary for methods that don't need to return anything
86
+ end(res, 200, statusCodeOverride)
87
+ } else {
88
+ // if the returned object has known fields, treat it as a response object instead of the body
89
+ if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
90
+ if (handled.headers) {
91
+ res.assignHeaders(handled.headers)
92
+ }
93
+ res.statusMessage = handled.statusMessage || res.statusMessage
94
+ end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
95
+ } else {
96
+ end(res, 200, statusCodeOverride, handled)
97
+ }
98
+ }
99
+ res.finalized = true
100
+ }
101
+ }
102
+
103
+ const pipeResponse = (res, readStream, errorTransformer) => {
104
+ readStream.on('data', res.write)
105
+ .on('end', res.end)
106
+ .on('error', e => {
107
+ try {
108
+ readStream.destroy()
109
+ } finally {
110
+ endError(res, e, uuid(), errorTransformer)
111
+ }
112
+ })
113
+ }
114
+
115
+ const doSerializeBody = (body, res) => {
116
+ if (!body || typeof body === 'string' || body instanceof Readable) {
117
+ return body
118
+ }
119
+ const contentType = res.getHeader('content-type')
120
+ const serialized = serializeBody(body, contentType, res.acceptsDefault)
121
+
122
+ if (serialized?.contentType && !contentType) {
123
+ res.headers['content-type'] = serialized.contentType
124
+ }
125
+ return serialized?.data || ''
126
+ }
127
+
128
+ async function executeMiddleware (middleware, req, res, errorTransformer, refId, e) {
129
+ if (!middleware) return
130
+
131
+ let applicableMiddleware = middleware[req.method]
132
+ if (middleware.ALL) {
133
+ if (applicableMiddleware) applicableMiddleware = middleware.ALL.concat(applicableMiddleware)
134
+ else applicableMiddleware = middleware.ALL
135
+ }
136
+
137
+ if (!applicableMiddleware || applicableMiddleware.length === 0) {
138
+ return
139
+ }
140
+ if (e) {
141
+ await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 4), req, res, e)
142
+ } else {
143
+ await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 3), req, res)
144
+ }
145
+ }
146
+
147
+ const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
148
+ try {
149
+ let reqBody
150
+ if (!handler.streamRequestBody) {
151
+ let buffer
152
+ reqBody = new Promise(
153
+ resolve =>
154
+ res.onData(async (data, isLast) => {
155
+ if (isLast) {
156
+ buffer = data.byteLength > 0 ? Buffer.concat([buffer, Buffer.from(data)].filter(b => b)) : buffer
157
+ resolve(buffer)
158
+ }
159
+ buffer = Buffer.concat([buffer, Buffer.from(data)].filter(b => b))
160
+ })
161
+ )
162
+ } else {
163
+ const readable = new Readable({
164
+ read: () => {
165
+ }
166
+ })
167
+ res.onData(async (data, isLast) => {
168
+ if (data.byteLength === 0 && !isLast) return
169
+ // data must be copied so it isn't lost
170
+ readable.push(Buffer.concat([Buffer.from(data)]))
171
+ if (isLast) {
172
+ readable.push(null)
173
+ }
174
+ })
175
+ reqBody = Promise.resolve(readable)
176
+ }
177
+ await executeMiddleware(middleware, req, res, errorTransformer)
178
+ if (!res.writableEnded && !res.ended) {
179
+ await executeHandler(req.spliffyUrl, res, req, reqBody, handler, middleware, errorTransformer)
180
+ }
181
+ } catch (e) {
182
+ const refId = uuid()
183
+ await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
184
+ if (!res.writableEnded) { endError(res, e, refId, errorTransformer) }
185
+ }
186
+ }
187
+
188
+ export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
189
+
190
+ let currentDate = new Date().toISOString()
191
+ setInterval(() => { currentDate = new Date().toISOString() }, 1000)
192
+
193
+ export const createHandler = (handler, middleware, pathParameters, config) => function (res, req) {
194
+ try {
195
+ res.cork(() => {
196
+ req = decorateRequest(req, pathParameters, res, config)
197
+ res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
198
+
199
+ if (config.logAccess) {
200
+ res.onEnd = writeAccess(req, res)
201
+ }
202
+
203
+ if (config.writeDateHeader) {
204
+ res.headers.date = currentDate
205
+ }
206
+
207
+ handleRequest(req, res, handler, middleware, config.errorTransformer)
208
+ .catch(e => {
209
+ log.error('Failed handling request', e)
210
+ res.statusCode = 500
211
+ res.end()
212
+ })
213
+ })
214
+ } catch (e) {
215
+ log.error('Failed handling request', e)
216
+ res.statusCode = 500
217
+ res.end()
218
+ }
219
+ }
220
+
221
+ export const createNotFoundHandler = config => {
222
+ const handler = config.defaultRouteHandler || config.notFoundRouteHandler
223
+ const params = handler?.pathParameters || []
224
+ return (res, req) => {
225
+ try {
226
+ req = decorateRequest(req, params, res, config)
227
+ res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
228
+ if (config.logAccess) {
229
+ res.onEnd = writeAccess(req, res)
230
+ }
231
+ if (handler && typeof handler === 'object') {
232
+ if (handler.handlers && typeof handler.handlers[req.method] === 'function') {
233
+ if ('statusCodeOverride' in handler) {
234
+ handler.handlers[req.method].statusCodeOverride = handler.statusCodeOverride
235
+ }
236
+ handleRequest(req, res,
237
+ handler.handlers[req.method],
238
+ handler.middleware,
239
+ config.errorTransformer
240
+ ).catch((e) => {
241
+ log.error('Unexpected exception during request handling', e)
242
+ res.statusCode = 500
243
+ })
244
+ } else {
245
+ res.statusCode = 404
246
+ res.end()
247
+ }
248
+ } else {
249
+ res.statusCode = 404
250
+ res.end()
251
+ }
252
+ } catch (e) {
253
+ log.error('Failed handling request', e)
254
+ }
255
+ }
256
+ }
package/src/index.mjs CHANGED
@@ -1,31 +1,31 @@
1
- import path from 'path'
2
- import { fileURLToPath } from 'url'
3
-
4
- /**
5
- * A helper for creating a redirect handler
6
- * @param location The location to redirect to
7
- * @param permanent Whether this is a permanent redirect or not
8
- */
9
- export const redirect = (location, permanent = true) => () => ({
10
- statusCode: permanent ? 301 : 302,
11
- headers: {
12
- location: location
13
- }
14
- })
15
-
16
- export { default as log } from './log.mjs'
17
- export { parseQuery, setMultiValueKey } from './url.mjs'
18
-
19
- /**
20
- * Startup function for the spliffy server
21
- * @param config See https://github.com/narcolepticsnowman/spliffy#config
22
- * @returns {Promise<Server>} Either the https server if https is configured or the http server
23
- */
24
- export { default } from './start.mjs'
25
-
26
- /**
27
- * Get the dirname for the given meta url
28
- * @param metaUrl The import.meta.url value to get the dirname from
29
- * @return {string} The full path to the directory the module is in
30
- */
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ /**
5
+ * A helper for creating a redirect handler
6
+ * @param location The location to redirect to
7
+ * @param permanent Whether this is a permanent redirect or not
8
+ */
9
+ export const redirect = (location, permanent = true) => () => ({
10
+ statusCode: permanent ? 301 : 302,
11
+ headers: {
12
+ location: location
13
+ }
14
+ })
15
+
16
+ export { default as log } from './log.mjs'
17
+ export { parseQuery, setMultiValueKey } from './url.mjs'
18
+
19
+ /**
20
+ * Startup function for the spliffy server
21
+ * @param config See https://github.com/narcolepticsnowman/spliffy#config
22
+ * @returns {Promise<Server>} Either the https server if https is configured or the http server
23
+ */
24
+ export { default } from './start.mjs'
25
+
26
+ /**
27
+ * Get the dirname for the given meta url
28
+ * @param metaUrl The import.meta.url value to get the dirname from
29
+ * @return {string} The full path to the directory the module is in
30
+ */
31
31
  export const moduleDirname = metaUrl => path.dirname(fileURLToPath(metaUrl))