@srfnstack/spliffy 1.2.9 → 1.4.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.
- package/package.json +4 -2
- package/src/decorator.mjs +181 -180
- package/src/handler.mjs +156 -164
- package/src/middleware.mjs +30 -17
- package/src/routes.mjs +8 -0
- package/src/server.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srfnstack/spliffy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"author": "snowbldr",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/SRFNStack/spliffy",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "npm run lint:fix && jest",
|
|
24
24
|
"lint": "standard --env jest src ./*js && standard --env jest --env browser --global Prism docs example",
|
|
25
|
-
"lint:fix": "standard --env jest --fix src ./*js && standard --env jest --env browser --global Prism --fix docs example"
|
|
25
|
+
"lint:fix": "standard --env jest --fix src ./*js && standard --env jest --env browser --global Prism --fix docs example",
|
|
26
|
+
"bench": "node bench/compare.mjs"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"node",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.56.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
43
|
+
"autocannon": "^8.0.0",
|
|
42
44
|
"helmet": "^4.6.0",
|
|
43
45
|
"jest": "^27.3.1",
|
|
44
46
|
"node-fetch": "^2.6.7",
|
package/src/decorator.mjs
CHANGED
|
@@ -1,213 +1,214 @@
|
|
|
1
1
|
import cookie from 'cookie'
|
|
2
|
-
import http from 'http'
|
|
3
2
|
import { parseQuery } from './url.mjs'
|
|
4
|
-
import log from './log.mjs'
|
|
5
3
|
import { v4 as uuid } from 'uuid'
|
|
6
4
|
import stream from 'stream'
|
|
7
|
-
import
|
|
5
|
+
import { defaultStatusMessages } from './httpStatusCodes.mjs'
|
|
8
6
|
|
|
9
7
|
const { Writable } = stream
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
destroy: true,
|
|
16
|
-
_addHeaderLines: true,
|
|
17
|
-
_addHeaderLine: true,
|
|
18
|
-
_dump: true,
|
|
19
|
-
__proto__: true
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const normalizeHeader = header => header.toLowerCase()
|
|
9
|
+
export class SpliffyRequest {
|
|
10
|
+
constructor (uwsReq, paramToIndex, res, config) {
|
|
11
|
+
this.init(uwsReq, paramToIndex, res, config)
|
|
12
|
+
}
|
|
23
13
|
|
|
24
|
-
|
|
14
|
+
init (uwsReq, paramToIndex, res, config, method, urlPath) {
|
|
15
|
+
this.res = res
|
|
16
|
+
this._uwsReq = uwsReq
|
|
17
|
+
this._config = config
|
|
18
|
+
this._paramToIndex = paramToIndex
|
|
19
|
+
this.path = urlPath || (uwsReq ? uwsReq.getUrl() : '')
|
|
20
|
+
this.method = method || (uwsReq ? uwsReq.getMethod().toUpperCase() : '')
|
|
21
|
+
this._url = null
|
|
22
|
+
this._headers = null
|
|
23
|
+
this._spliffyUrl = null
|
|
24
|
+
this._cookies = null
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
forceCache () {
|
|
28
|
+
if (this._headers === null) this._cacheHeaders()
|
|
29
|
+
if (this._spliffyUrl === null) this._cacheSpliffyUrl()
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
extendIncomingMessage
|
|
35
|
-
} = {}) {
|
|
36
|
-
// uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
|
|
37
|
-
const req = {}
|
|
38
|
-
if (extendIncomingMessage) {
|
|
39
|
-
// frameworks like passport like to modify the message prototype
|
|
40
|
-
// Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
|
|
41
|
-
for (const p of reqProtoProps()) {
|
|
42
|
-
if (!req[p]) req[p] = http.IncomingMessage.prototype[p]
|
|
32
|
+
_cacheHeaders () {
|
|
33
|
+
if (this._headers === null) {
|
|
34
|
+
this._headers = {}
|
|
35
|
+
this._uwsReq.forEach((header, value) => { this._headers[header] = value })
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
|
-
const query = uwsReq.getQuery()
|
|
46
|
-
req.path = uwsReq.getUrl()
|
|
47
|
-
req.url = `${req.path}${query ? '?' + query : ''}`
|
|
48
|
-
const paramToIndex = pathParameters.reduce((acc, cur, i) => {
|
|
49
|
-
acc[cur] = i
|
|
50
|
-
return acc
|
|
51
|
-
}, {})
|
|
52
|
-
req.spliffyUrl = {
|
|
53
|
-
path: req.path,
|
|
54
|
-
query: (query && parseQuery(query, decodeQueryParameters)) || {},
|
|
55
|
-
param: name => uwsReq.getParameter(paramToIndex[name])
|
|
56
|
-
}
|
|
57
|
-
req.query = req.spliffyUrl.query
|
|
58
|
-
req.headers = {}
|
|
59
|
-
uwsReq.forEach((header, value) => { req.headers[header] = value })
|
|
60
|
-
req.method = uwsReq.getMethod().toUpperCase()
|
|
61
|
-
req.remoteAddress = addressArrayBufferToString(res.getRemoteAddressAsText())
|
|
62
|
-
req.proxiedRemoteAddress = addressArrayBufferToString(res.getProxiedRemoteAddressAsText())
|
|
63
|
-
req.get = header => req.headers[header]
|
|
64
|
-
if (parseCookie) {
|
|
65
|
-
req.cookies = (req.headers.cookie && cookie.parse(req.headers.cookie)) || {}
|
|
66
|
-
}
|
|
67
|
-
return req
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function toArrayBuffer (buffer) {
|
|
71
|
-
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
|
|
72
|
-
}
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
res.removeHeader = header => {
|
|
88
|
-
delete res.headers[normalizeHeader(header)]
|
|
89
|
-
}
|
|
90
|
-
res.flushHeaders = () => {
|
|
91
|
-
if (res.headersSent) return
|
|
92
|
-
if (!res.statusCode) res.statusCode = httpStatusCodes.OK
|
|
93
|
-
if (!res.statusMessage) res.statusMessage = defaultStatusMessages[res.statusCode]
|
|
94
|
-
res.headersSent = true
|
|
95
|
-
res.writeStatus(`${res.statusCode} ${res.statusMessage}`)
|
|
96
|
-
if (typeof res.onFlushHeaders === 'function') {
|
|
97
|
-
res.onFlushHeaders(res)
|
|
98
|
-
}
|
|
99
|
-
for (const header of Object.keys(res.headers)) {
|
|
100
|
-
if (Array.isArray(res.headers[header])) {
|
|
101
|
-
for (const multiple of res.headers[header]) {
|
|
102
|
-
res.writeHeader(header, multiple.toString())
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
res.writeHeader(header, res.headers[header].toString())
|
|
39
|
+
_cacheSpliffyUrl () {
|
|
40
|
+
if (this._spliffyUrl === null) {
|
|
41
|
+
const q = this._uwsReq.getQuery()
|
|
42
|
+
const p2i = this._paramToIndex
|
|
43
|
+
const params = {}
|
|
44
|
+
for (const name in p2i) {
|
|
45
|
+
params[name] = this._uwsReq.getParameter(p2i[name])
|
|
46
|
+
}
|
|
47
|
+
this._spliffyUrl = {
|
|
48
|
+
path: this.path,
|
|
49
|
+
query: (q && parseQuery(q, this._config.decodeQueryParameters)) || {},
|
|
50
|
+
param: name => params[name]
|
|
106
51
|
}
|
|
107
52
|
}
|
|
108
53
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
54
|
+
|
|
55
|
+
get url () {
|
|
56
|
+
if (this._url === null) {
|
|
57
|
+
const q = this._uwsReq.getQuery()
|
|
58
|
+
this._url = q ? `${this.path}?${q}` : this.path
|
|
59
|
+
}
|
|
60
|
+
return this._url
|
|
112
61
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
62
|
+
|
|
63
|
+
get headers () {
|
|
64
|
+
if (this._headers === null) this._cacheHeaders()
|
|
65
|
+
return this._headers
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get spliffyUrl () {
|
|
69
|
+
if (this._spliffyUrl === null) this._cacheSpliffyUrl()
|
|
70
|
+
return this._spliffyUrl
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get query () { return this.spliffyUrl.query }
|
|
74
|
+
get cookies () {
|
|
75
|
+
if (this._cookies === null) {
|
|
76
|
+
this._cookies = (this.headers.cookie && cookie.parse(this.headers.cookie)) || {}
|
|
116
77
|
}
|
|
78
|
+
return this._cookies
|
|
117
79
|
}
|
|
118
|
-
|
|
119
|
-
|
|
80
|
+
|
|
81
|
+
get remoteAddress () {
|
|
82
|
+
const val = Buffer.from(this.res.getRemoteAddressAsText()).toString()
|
|
83
|
+
Object.defineProperty(this, 'remoteAddress', { value: val, enumerable: true })
|
|
84
|
+
return val
|
|
120
85
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
86
|
+
|
|
87
|
+
get proxiedRemoteAddress () {
|
|
88
|
+
const val = Buffer.from(this.res.getProxiedRemoteAddressAsText()).toString()
|
|
89
|
+
Object.defineProperty(this, 'proxiedRemoteAddress', { value: val, enumerable: true })
|
|
90
|
+
return val
|
|
124
91
|
}
|
|
125
92
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
93
|
+
get (header) { return this.headers[header.toLowerCase()] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function decorateRequest (uwsReq, paramToIndex, res, config) {
|
|
97
|
+
return new SpliffyRequest(uwsReq, paramToIndex, res, config)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const resProto = {
|
|
101
|
+
setHeader (h, v) { this.headers[h.toLowerCase()] = v },
|
|
102
|
+
removeHeader (h) { delete this.headers[h.toLowerCase()] },
|
|
103
|
+
assignHeaders (headers) {
|
|
104
|
+
for (const h in headers) { this.headers[h.toLowerCase()] = headers[h] }
|
|
105
|
+
},
|
|
106
|
+
getHeader (h) { return this.headers[h.toLowerCase()] },
|
|
107
|
+
status (c) { this.statusCode = c; return this },
|
|
108
|
+
writeHead (s, h) { this.statusCode = s; if (h) this.assignHeaders(h) },
|
|
109
|
+
redirect (c, l) {
|
|
110
|
+
if (typeof c === 'string') { l = c; c = 301 }
|
|
111
|
+
return this._finalizeResponse(this._sReq, this, { statusCode: c, headers: { location: l } })
|
|
112
|
+
},
|
|
113
|
+
send (b) { return this._finalizeResponse(this._sReq, this, b) },
|
|
114
|
+
json (b) { return this._finalizeResponse(this._sReq, this, b) },
|
|
115
|
+
setCookie (n, v, o) {
|
|
116
|
+
const s = cookie.serialize(n, v, o)
|
|
117
|
+
const e = this.headers['set-cookie']
|
|
118
|
+
if (e) {
|
|
119
|
+
if (Array.isArray(e)) e.push(s)
|
|
120
|
+
else this.headers['set-cookie'] = [e, s]
|
|
121
|
+
} else this.headers['set-cookie'] = s
|
|
122
|
+
},
|
|
123
|
+
cookie (n, v, o) { return this.setCookie(n, v, o) },
|
|
124
|
+
flushHeaders (noCork) {
|
|
125
|
+
if (this.headersSent) return
|
|
126
|
+
this.headersSent = true
|
|
127
|
+
const flush = () => {
|
|
128
|
+
if (this.statusCode && this.statusCode !== 200) {
|
|
129
|
+
this.writeStatus(this.statusCode + ' ' + (defaultStatusMessages[this.statusCode] || ''))
|
|
130
|
+
}
|
|
131
|
+
const headers = this.headers
|
|
132
|
+
for (const h in headers) {
|
|
133
|
+
const v = headers[h]
|
|
134
|
+
if (Array.isArray(v)) {
|
|
135
|
+
for (let i = 0, l = v.length; i < l; i++) this.writeHeader(h, String(v[i]))
|
|
148
136
|
} else {
|
|
149
|
-
|
|
137
|
+
this.writeHeader(h, String(v))
|
|
150
138
|
}
|
|
151
139
|
}
|
|
140
|
+
}
|
|
141
|
+
if (noCork) flush()
|
|
142
|
+
else this.cork(flush)
|
|
143
|
+
},
|
|
144
|
+
ensureOnAborted () {
|
|
145
|
+
if (this._onAbortedRegistered) return
|
|
146
|
+
this._onAbortedRegistered = true
|
|
147
|
+
this.onAborted(() => { this.ended = true; this.writableEnded = true; this.finalized = true })
|
|
148
|
+
},
|
|
149
|
+
write (chunk, encoding, cb) {
|
|
150
|
+
this.ensureOnAborted()
|
|
151
|
+
this.streaming = true
|
|
152
|
+
return this.cork(() => {
|
|
153
|
+
if (!this.headersSent) this.flushHeaders(true)
|
|
154
|
+
const result = this._uwsWrite((chunk instanceof Buffer || chunk instanceof Uint8Array || typeof chunk === 'string') ? chunk : JSON.stringify(chunk))
|
|
155
|
+
if (cb) cb()
|
|
156
|
+
return result
|
|
152
157
|
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
158
|
+
},
|
|
159
|
+
end (body) {
|
|
160
|
+
if (this.ended) return
|
|
161
|
+
this.ended = true
|
|
162
|
+
this.writableEnded = true
|
|
163
|
+
this.cork(() => {
|
|
164
|
+
if (!this.headersSent) this.flushHeaders(true)
|
|
165
|
+
this._uwsEnd(body || '')
|
|
166
|
+
})
|
|
167
|
+
if (this.onEnd) this.onEnd()
|
|
168
|
+
},
|
|
169
|
+
getWritable () {
|
|
170
|
+
this.ensureOnAborted()
|
|
171
|
+
if (!this.outStream) {
|
|
172
|
+
this.streaming = true
|
|
173
|
+
this.outStream = new Writable({ write: (c, e, callback) => { this.write(c, e, callback) } })
|
|
174
|
+
.on('finish', () => this.end())
|
|
175
|
+
.on('error', e => { try { this.outStream.destroy() } finally { this._endError(this, e, uuid(), this._errorTransformer) } })
|
|
170
176
|
}
|
|
171
|
-
return outStream
|
|
177
|
+
return this.outStream
|
|
172
178
|
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function decorateResponse (res, sReq, finalizeResponse, errorTransformer, endError, config) {
|
|
182
|
+
res._sReq = sReq
|
|
183
|
+
res._finalizeResponse = finalizeResponse
|
|
184
|
+
res._errorTransformer = errorTransformer
|
|
185
|
+
res._endError = endError
|
|
186
|
+
res._onAbortedRegistered = false
|
|
187
|
+
res.ensureOnAborted = resProto.ensureOnAborted
|
|
188
|
+
res.acceptsDefault = config.acceptsDefault
|
|
189
|
+
res.headers = {}
|
|
190
|
+
res.headersSent = false
|
|
191
|
+
res.setHeader = resProto.setHeader
|
|
192
|
+
res.removeHeader = resProto.removeHeader
|
|
193
|
+
res.assignHeaders = resProto.assignHeaders
|
|
194
|
+
res.getHeader = resProto.getHeader
|
|
195
|
+
res.status = resProto.status
|
|
196
|
+
res.writeHead = resProto.writeHead
|
|
197
|
+
res.flushHeaders = resProto.flushHeaders
|
|
198
|
+
res.redirect = resProto.redirect
|
|
199
|
+
res.send = resProto.send
|
|
200
|
+
res.json = resProto.json
|
|
201
|
+
res.setCookie = resProto.setCookie
|
|
202
|
+
res.cookie = resProto.cookie
|
|
203
|
+
|
|
204
|
+
res._uwsWrite = res.write
|
|
205
|
+
res.write = resProto.write
|
|
206
|
+
|
|
207
|
+
res.getWritable = resProto.getWritable
|
|
173
208
|
|
|
174
|
-
|
|
209
|
+
res._uwsEnd = res.end
|
|
175
210
|
res.ended = false
|
|
176
|
-
res.end =
|
|
177
|
-
if (res.ended) {
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
// provide writableEnded like node does, with slightly different behavior
|
|
181
|
-
if (!res.writableEnded) {
|
|
182
|
-
res.cork(() => {
|
|
183
|
-
res.flushHeaders()
|
|
184
|
-
uwsEnd.call(res, body)
|
|
185
|
-
res.writableEnded = true
|
|
186
|
-
res.ended = true
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
if (typeof res.onEnd === 'function') {
|
|
190
|
-
res.onEnd()
|
|
191
|
-
}
|
|
192
|
-
}
|
|
211
|
+
res.end = resProto.end
|
|
193
212
|
|
|
194
|
-
res.redirect = function (code, location) {
|
|
195
|
-
if (arguments.length === 1) {
|
|
196
|
-
location = code
|
|
197
|
-
code = httpStatusCodes.MOVED_PERMANENTLY
|
|
198
|
-
}
|
|
199
|
-
return finalizeResponse(req, res, {
|
|
200
|
-
statusCode: code,
|
|
201
|
-
headers: {
|
|
202
|
-
location: location
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
res.send = (body) => {
|
|
207
|
-
finalizeResponse(req, res, body)
|
|
208
|
-
}
|
|
209
|
-
res.json = res.send
|
|
210
|
-
res.setCookie = setCookie(res)
|
|
211
|
-
res.cookie = res.setCookie
|
|
212
213
|
return res
|
|
213
214
|
}
|
package/src/handler.mjs
CHANGED
|
@@ -1,40 +1,29 @@
|
|
|
1
1
|
import log from './log.mjs'
|
|
2
2
|
import { deserializeBody, serializeBody } from './content.mjs'
|
|
3
|
-
import { invokeMiddleware } from './middleware.mjs'
|
|
4
|
-
import { decorateResponse,
|
|
3
|
+
import { invokeMiddleware, preProcessMiddleware } from './middleware.mjs'
|
|
4
|
+
import { decorateResponse, SpliffyRequest } from './decorator.mjs'
|
|
5
5
|
import { v4 as uuid } from 'uuid'
|
|
6
6
|
import stream from 'stream'
|
|
7
7
|
const { Readable } = stream
|
|
8
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
|
-
}
|
|
9
|
+
const NULL_PROMISE = Promise.resolve(null)
|
|
31
10
|
|
|
11
|
+
const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
|
|
32
12
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
13
|
+
const bodyContent = await (bodyPromise || NULL_PROMISE)
|
|
14
|
+
if (!res.writableEnded) {
|
|
15
|
+
const deserializedBody = (bodyContent instanceof Readable || !bodyContent)
|
|
16
|
+
? bodyContent
|
|
17
|
+
: deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
|
|
18
|
+
|
|
19
|
+
const handled = await handler({ url, bodyPromise: Promise.resolve(deserializedBody), headers: req.headers, req, res })
|
|
20
|
+
finalizeResponse(req, res, handled, handler.statusCodeOverride)
|
|
21
|
+
}
|
|
35
22
|
} catch (e) {
|
|
36
23
|
const refId = uuid()
|
|
37
|
-
|
|
24
|
+
if (middleware) {
|
|
25
|
+
try { await executeMiddleware(middleware, req, res, errorTransformer, refId, e) } catch (me) { log.error(me) }
|
|
26
|
+
}
|
|
38
27
|
endError(res, e, refId, errorTransformer)
|
|
39
28
|
}
|
|
40
29
|
}
|
|
@@ -48,199 +37,202 @@ const endError = (res, e, refId, errorTransformer) => {
|
|
|
48
37
|
}
|
|
49
38
|
res.headers['x-ref-id'] = refId
|
|
50
39
|
const status = e.statusCode || 500
|
|
51
|
-
if (status === 500)
|
|
52
|
-
log.error(e)
|
|
53
|
-
}
|
|
40
|
+
if (status === 500) log.error(e)
|
|
54
41
|
end(res, status, null, e.body || '')
|
|
55
42
|
}
|
|
56
43
|
|
|
57
44
|
const end = (res, defaultStatusCode, statusCodeOverride, body) => {
|
|
58
|
-
// status set directly on res wins
|
|
59
45
|
res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
|
|
46
|
+
if (currentDate) res.setHeader('Date', currentDate)
|
|
60
47
|
if (body instanceof Readable || res.streaming) {
|
|
61
48
|
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
|
|
49
|
+
if (body instanceof Readable) pipeResponse(res, body)
|
|
66
50
|
} else {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
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')
|
|
51
|
+
if (typeof body === 'string' || !body || body instanceof Buffer) {
|
|
52
|
+
res.end(body || '')
|
|
53
|
+
} else {
|
|
54
|
+
res.end(doSerializeBody(body, res) || '')
|
|
55
|
+
}
|
|
79
56
|
}
|
|
80
57
|
}
|
|
81
58
|
|
|
82
59
|
const finalizeResponse = (req, res, handled, statusCodeOverride) => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
|
|
95
|
-
} else {
|
|
96
|
-
end(res, 200, statusCodeOverride, handled)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
res.finalized = true
|
|
60
|
+
if (res.finalized) return
|
|
61
|
+
res.finalized = true
|
|
62
|
+
|
|
63
|
+
if (typeof handled === 'string' || !handled || handled instanceof Buffer) {
|
|
64
|
+
end(res, 200, statusCodeOverride, handled)
|
|
65
|
+
} else if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
|
|
66
|
+
if (handled.headers) res.assignHeaders(handled.headers)
|
|
67
|
+
if (handled.statusMessage) res.statusMessage = handled.statusMessage
|
|
68
|
+
end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
|
|
69
|
+
} else {
|
|
70
|
+
end(res, 200, statusCodeOverride, handled)
|
|
100
71
|
}
|
|
101
72
|
}
|
|
102
73
|
|
|
103
74
|
const pipeResponse = (res, readStream, errorTransformer) => {
|
|
104
|
-
readStream.on('data', res.write)
|
|
105
|
-
.on('end', res.end)
|
|
75
|
+
readStream.on('data', chunk => res.write(chunk))
|
|
76
|
+
.on('end', () => res.end())
|
|
106
77
|
.on('error', e => {
|
|
107
|
-
try {
|
|
108
|
-
readStream.destroy()
|
|
109
|
-
} finally {
|
|
110
|
-
endError(res, e, uuid(), errorTransformer)
|
|
111
|
-
}
|
|
78
|
+
try { readStream.destroy() } finally { endError(res, e, uuid(), errorTransformer) }
|
|
112
79
|
})
|
|
113
80
|
}
|
|
114
81
|
|
|
115
82
|
const doSerializeBody = (body, res) => {
|
|
116
|
-
|
|
117
|
-
return body
|
|
118
|
-
}
|
|
119
|
-
const contentType = res.getHeader('content-type')
|
|
83
|
+
const contentType = res.headers['content-type']
|
|
120
84
|
const serialized = serializeBody(body, contentType, res.acceptsDefault)
|
|
121
|
-
|
|
122
85
|
if (serialized?.contentType && !contentType) {
|
|
123
86
|
res.headers['content-type'] = serialized.contentType
|
|
124
87
|
}
|
|
125
88
|
return serialized?.data || ''
|
|
126
89
|
}
|
|
127
90
|
|
|
128
|
-
async function executeMiddleware (
|
|
129
|
-
if (!
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
}
|
|
91
|
+
async function executeMiddleware (processedMiddleware, req, res, errorTransformer, refId, e) {
|
|
92
|
+
if (!processedMiddleware) return
|
|
93
|
+
const methodMiddleware = processedMiddleware[req.method]
|
|
94
|
+
const allMiddleware = processedMiddleware.ALL
|
|
140
95
|
if (e) {
|
|
141
|
-
await invokeMiddleware(
|
|
96
|
+
if (allMiddleware?.error) await invokeMiddleware(allMiddleware.error, req, res, e)
|
|
97
|
+
if (methodMiddleware?.error) await invokeMiddleware(methodMiddleware.error, req, res, e)
|
|
142
98
|
} else {
|
|
143
|
-
await invokeMiddleware(
|
|
99
|
+
if (allMiddleware?.normal) await invokeMiddleware(allMiddleware.normal, req, res)
|
|
100
|
+
if (methodMiddleware?.normal) await invokeMiddleware(methodMiddleware.normal, req, res)
|
|
144
101
|
}
|
|
145
102
|
}
|
|
146
103
|
|
|
147
|
-
const
|
|
104
|
+
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
|
|
105
|
+
|
|
106
|
+
let currentDate = ''
|
|
107
|
+
let dateInterval = null
|
|
108
|
+
|
|
109
|
+
const handleRequest = async (sReq, res, handler, processedMiddleware, config) => {
|
|
148
110
|
try {
|
|
149
|
-
let
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
resolve
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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)
|
|
111
|
+
let reqBodyPromise = NULL_PROMISE
|
|
112
|
+
if (sReq.method !== 'GET' && sReq.method !== 'HEAD') {
|
|
113
|
+
if (handler.streamRequestBody) {
|
|
114
|
+
const readable = new Readable({ read () {} })
|
|
115
|
+
reqBodyPromise = Promise.resolve(readable)
|
|
116
|
+
res.onData((data, isLast) => {
|
|
117
|
+
if (data.byteLength > 0 || isLast) {
|
|
118
|
+
readable.push(Buffer.concat([Buffer.from(data)]))
|
|
119
|
+
if (isLast) readable.push(null)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
} else {
|
|
123
|
+
reqBodyPromise = new Promise(resolve => res.onData((data, isLast) => {
|
|
124
|
+
const chunk = Buffer.concat([Buffer.from(data)])
|
|
125
|
+
res._buffer = res._buffer ? Buffer.concat([res._buffer, chunk]) : chunk
|
|
126
|
+
if (isLast) resolve(res._buffer)
|
|
127
|
+
}))
|
|
128
|
+
}
|
|
176
129
|
}
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
await
|
|
130
|
+
|
|
131
|
+
if (processedMiddleware) {
|
|
132
|
+
await executeMiddleware(processedMiddleware, sReq, res, config.errorTransformer)
|
|
133
|
+
if (!res.writableEnded && !res.ended) {
|
|
134
|
+
await executeHandler(sReq.spliffyUrl, res, sReq, reqBodyPromise, handler, processedMiddleware, config.errorTransformer)
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
await executeHandler(sReq.spliffyUrl, res, sReq, reqBodyPromise, handler, null, config.errorTransformer)
|
|
180
138
|
}
|
|
181
139
|
} catch (e) {
|
|
182
140
|
const refId = uuid()
|
|
183
|
-
|
|
184
|
-
|
|
141
|
+
if (processedMiddleware) {
|
|
142
|
+
await executeMiddleware(processedMiddleware, sReq, res, config.errorTransformer, refId, e)
|
|
143
|
+
}
|
|
144
|
+
if (!res.writableEnded) { endError(res, e, refId, config.errorTransformer) }
|
|
185
145
|
}
|
|
186
146
|
}
|
|
187
147
|
|
|
188
|
-
export const
|
|
148
|
+
export const createHandler = (handler, middleware, paramToIndex, config, urlPath) => {
|
|
149
|
+
const processedMiddleware = preProcessMiddleware(middleware)
|
|
150
|
+
if (config.writeDateHeader && !dateInterval) {
|
|
151
|
+
currentDate = new Date().toUTCString()
|
|
152
|
+
dateInterval = setInterval(() => { currentDate = new Date().toUTCString() }, 1000)
|
|
153
|
+
}
|
|
189
154
|
|
|
190
|
-
|
|
191
|
-
setInterval(() => { currentDate = new Date().toISOString() }, 1000)
|
|
155
|
+
const isWildcardPath = urlPath?.indexOf('*') > -1
|
|
192
156
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
157
|
+
// Pre-allocated request object for sync path
|
|
158
|
+
const syncSReq = new SpliffyRequest(null, paramToIndex, null, config)
|
|
159
|
+
const syncContext = {
|
|
160
|
+
get url () { return syncSReq.spliffyUrl },
|
|
161
|
+
bodyPromise: NULL_PROMISE,
|
|
162
|
+
get headers () { return syncSReq.headers },
|
|
163
|
+
req: syncSReq,
|
|
164
|
+
res: null
|
|
165
|
+
}
|
|
198
166
|
|
|
199
|
-
|
|
200
|
-
|
|
167
|
+
return function (res, req) {
|
|
168
|
+
try {
|
|
169
|
+
const rawMethod = req.getMethod()
|
|
170
|
+
if (!processedMiddleware && !handler.streamRequestBody && (rawMethod === 'get' || rawMethod === 'head')) {
|
|
171
|
+
// HYPER FAST PATH
|
|
172
|
+
const method = rawMethod === 'get' ? 'GET' : 'HEAD'
|
|
173
|
+
syncSReq.init(req, paramToIndex, res, config, method, isWildcardPath ? null : urlPath)
|
|
174
|
+
syncContext.res = res
|
|
175
|
+
decorateResponse(res, syncSReq, finalizeResponse, config.errorTransformer, endError, config)
|
|
176
|
+
const handled = handler(syncContext)
|
|
177
|
+
if (handled instanceof Promise) {
|
|
178
|
+
syncSReq.forceCache()
|
|
179
|
+
res.ensureOnAborted()
|
|
180
|
+
handled.then(h => finalizeResponse(syncSReq, res, h, handler.statusCodeOverride))
|
|
181
|
+
.catch(e => endError(res, e, uuid(), config.errorTransformer))
|
|
182
|
+
} else if (typeof handled === 'string' || handled instanceof Buffer) {
|
|
183
|
+
end(res, 200, handler.statusCodeOverride, handled)
|
|
184
|
+
} else {
|
|
185
|
+
finalizeResponse(syncSReq, res, handled, handler.statusCodeOverride)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const sReq = new SpliffyRequest(req, paramToIndex, res, config)
|
|
189
|
+
sReq.forceCache()
|
|
190
|
+
decorateResponse(res, sReq, finalizeResponse, config.errorTransformer, endError, config)
|
|
191
|
+
res.ensureOnAborted()
|
|
192
|
+
|
|
193
|
+
handleRequest(sReq, res, handler, processedMiddleware, config)
|
|
194
|
+
.catch(e => {
|
|
195
|
+
log.error('Failed handling request', e)
|
|
196
|
+
if (!res.writableEnded) {
|
|
197
|
+
res.statusCode = 500
|
|
198
|
+
res.end()
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
201
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
} catch (e) {
|
|
203
|
+
log.error('Failed handling request', e)
|
|
204
|
+
if (!res.writableEnded) {
|
|
205
|
+
res.statusCode = 500
|
|
206
|
+
res.end()
|
|
205
207
|
}
|
|
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()
|
|
208
|
+
}
|
|
218
209
|
}
|
|
219
210
|
}
|
|
220
211
|
|
|
221
212
|
export const createNotFoundHandler = config => {
|
|
222
213
|
const handler = config.defaultRouteHandler || config.notFoundRouteHandler
|
|
223
|
-
const
|
|
214
|
+
const paramToIndex = handler?.paramToIndex || {}
|
|
224
215
|
return (res, req) => {
|
|
225
216
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
217
|
+
const sReq = new SpliffyRequest(req, paramToIndex, res, config)
|
|
218
|
+
sReq.forceCache()
|
|
219
|
+
decorateResponse(res, sReq, finalizeResponse, config.errorTransformer, endError, config)
|
|
220
|
+
res.ensureOnAborted()
|
|
231
221
|
if (handler && typeof handler === 'object') {
|
|
232
|
-
|
|
222
|
+
const processedMiddleware = preProcessMiddleware(handler.middleware)
|
|
223
|
+
if (handler.handlers && typeof handler.handlers[sReq.method] === 'function') {
|
|
224
|
+
const h = handler.handlers[sReq.method]
|
|
233
225
|
if ('statusCodeOverride' in handler) {
|
|
234
|
-
|
|
226
|
+
h.statusCodeOverride = handler.statusCodeOverride
|
|
235
227
|
}
|
|
236
|
-
handleRequest(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
228
|
+
handleRequest(sReq, res, h, processedMiddleware, config)
|
|
229
|
+
.catch(e => {
|
|
230
|
+
log.error('Unexpected exception during request handling', e)
|
|
231
|
+
if (!res.writableEnded) {
|
|
232
|
+
res.statusCode = 500
|
|
233
|
+
res.end()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
244
236
|
} else {
|
|
245
237
|
res.statusCode = 404
|
|
246
238
|
res.end()
|
package/src/middleware.mjs
CHANGED
|
@@ -61,26 +61,39 @@ export const validateMiddlewareArray = (arr) => {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function preProcessMiddleware (middleware) {
|
|
65
|
+
if (!middleware) return null
|
|
66
|
+
const processed = {}
|
|
67
|
+
let hasAny = false
|
|
68
|
+
for (const method in middleware) {
|
|
69
|
+
const list = middleware[method]
|
|
70
|
+
if (list && list.length > 0) {
|
|
71
|
+
processed[method] = {
|
|
72
|
+
normal: list.filter(mw => mw.length <= 3),
|
|
73
|
+
error: list.filter(mw => mw.length === 4)
|
|
74
|
+
}
|
|
75
|
+
hasAny = true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return hasAny ? processed : null
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
export async function invokeMiddleware (middleware, req, res, reqErr) {
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
if (!middleware || middleware.length === 0) return
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let current = 0
|
|
67
85
|
const next = (err) => {
|
|
68
|
-
if (err) reject(err)
|
|
69
|
-
if (res.writableEnded) {
|
|
70
|
-
resolve()
|
|
71
|
-
return
|
|
86
|
+
if (err) return reject(err)
|
|
87
|
+
if (res.writableEnded || current === middleware.length) {
|
|
88
|
+
return resolve()
|
|
72
89
|
}
|
|
73
|
-
current++
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch (e) {
|
|
81
|
-
log.error('Middleware threw exception', e)
|
|
82
|
-
reject(e)
|
|
83
|
-
}
|
|
90
|
+
const mw = middleware[current++]
|
|
91
|
+
try {
|
|
92
|
+
if (reqErr) mw(reqErr, req, res, next)
|
|
93
|
+
else mw(req, res, next)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
log.error('Middleware threw exception', e)
|
|
96
|
+
reject(e)
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
|
package/src/routes.mjs
CHANGED
|
@@ -113,6 +113,10 @@ const buildJSHandlerRoute = async (name, filePath, urlPath, inheritedMiddleware,
|
|
|
113
113
|
}
|
|
114
114
|
const route = {
|
|
115
115
|
pathParameters,
|
|
116
|
+
paramToIndex: pathParameters.reduce((acc, cur, i) => {
|
|
117
|
+
acc[cur] = i
|
|
118
|
+
return acc
|
|
119
|
+
}, {}),
|
|
116
120
|
urlPath: `${urlPath}/${getPathPart(name)}`,
|
|
117
121
|
filePath,
|
|
118
122
|
handlers: {}
|
|
@@ -163,6 +167,10 @@ const buildStaticRoutes = (name, filePath, urlPath, inheritedMiddleware, pathPar
|
|
|
163
167
|
const contentType = getContentTypeByExtension(name, config.staticContentTypes)
|
|
164
168
|
const route = {
|
|
165
169
|
pathParameters,
|
|
170
|
+
paramToIndex: pathParameters.reduce((acc, cur, i) => {
|
|
171
|
+
acc[cur] = i
|
|
172
|
+
return acc
|
|
173
|
+
}, {}),
|
|
166
174
|
urlPath: `${urlPath}/${getPathPart(name)}`,
|
|
167
175
|
filePath,
|
|
168
176
|
handlers: createStaticHandler(filePath, contentType, config.cacheStatic, config.staticCacheControl),
|
package/src/server.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export async function startServer (config) {
|
|
|
102
102
|
if (method === 'WEBSOCKET') {
|
|
103
103
|
theHandler = route.handlers[method]
|
|
104
104
|
} else {
|
|
105
|
-
theHandler = createHandler(route.handlers[method], route.middleware, route.
|
|
105
|
+
theHandler = createHandler(route.handlers[method], route.middleware, route.paramToIndex, config, route.urlPath)
|
|
106
106
|
}
|
|
107
107
|
app[appMethods[method]](route.urlPath, theHandler)
|
|
108
108
|
if (hadSlash && config.serveRoutesWithSlash) {
|