@srfnstack/spliffy 1.3.0 → 1.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/spliffy",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "author": "snowbldr",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/SRFNStack/spliffy",
package/src/decorator.mjs CHANGED
@@ -1,207 +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 httpStatusCodes, { defaultStatusMessages } from './httpStatusCodes.mjs'
5
+ import { defaultStatusMessages } from './httpStatusCodes.mjs'
8
6
 
9
7
  const { Writable } = stream
10
8
 
11
- const addressArrayBufferToString = addrBuf => Buffer.from(addrBuf).toString()
12
-
13
- const excludedMessageProps = {
14
- setTimeout: true,
15
- _read: true,
16
- destroy: true,
17
- _addHeaderLines: true,
18
- _addHeaderLine: true,
19
- _dump: true,
20
- __proto__: true
21
- }
22
-
23
- const reqProtoProps = () => Object.keys(http.IncomingMessage.prototype).filter(p => !excludedMessageProps[p])
9
+ export class SpliffyRequest {
10
+ constructor (uwsReq, paramToIndex, res, config) {
11
+ this.init(uwsReq, paramToIndex, res, config)
12
+ }
24
13
 
25
- class SpliffyRequest {
26
- constructor (uwsReq, pathParameters, res, config) {
27
- const query = uwsReq.getQuery()
14
+ init (uwsReq, paramToIndex, res, config, method, urlPath) {
28
15
  this.res = res
29
- this.path = uwsReq.getUrl()
30
- this.url = query ? `${this.path}?${query}` : this.path
31
- this.method = uwsReq.getMethod().toUpperCase()
32
- this.headers = {}
33
- uwsReq.forEach((header, value) => {
34
- this.headers[header] = value
35
- })
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
+ }
26
+
27
+ forceCache () {
28
+ if (this._headers === null) this._cacheHeaders()
29
+ if (this._spliffyUrl === null) this._cacheSpliffyUrl()
30
+ }
31
+
32
+ _cacheHeaders () {
33
+ if (this._headers === null) {
34
+ this._headers = {}
35
+ this._uwsReq.forEach((header, value) => { this._headers[header] = value })
36
+ }
37
+ }
36
38
 
37
- if (config.extendIncomingMessage) {
38
- for (const p of reqProtoProps()) {
39
- if (!this[p]) this[p] = http.IncomingMessage.prototype[p]
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]
40
51
  }
41
52
  }
53
+ }
42
54
 
43
- const paramToIndex = pathParameters.reduce((acc, cur, i) => {
44
- acc[cur] = i
45
- return acc
46
- }, {})
47
-
48
- this.spliffyUrl = {
49
- path: this.path,
50
- query: (query && parseQuery(query, config.decodeQueryParameters)) || {},
51
- param: name => uwsReq.getParameter(paramToIndex[name])
55
+ get url () {
56
+ if (this._url === null) {
57
+ const q = this._uwsReq.getQuery()
58
+ this._url = q ? `${this.path}?${q}` : this.path
52
59
  }
53
- this.query = this.spliffyUrl.query
54
- if (config.parseCookie) {
55
- this.cookies = (this.headers.cookie && cookie.parse(this.headers.cookie)) || {}
60
+ return this._url
61
+ }
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)) || {}
56
77
  }
78
+ return this._cookies
57
79
  }
58
80
 
59
81
  get remoteAddress () {
60
- const val = addressArrayBufferToString(this.res.getRemoteAddressAsText())
82
+ const val = Buffer.from(this.res.getRemoteAddressAsText()).toString()
61
83
  Object.defineProperty(this, 'remoteAddress', { value: val, enumerable: true })
62
84
  return val
63
85
  }
64
86
 
65
87
  get proxiedRemoteAddress () {
66
- const val = addressArrayBufferToString(this.res.getProxiedRemoteAddressAsText())
88
+ const val = Buffer.from(this.res.getProxiedRemoteAddressAsText()).toString()
67
89
  Object.defineProperty(this, 'proxiedRemoteAddress', { value: val, enumerable: true })
68
90
  return val
69
91
  }
70
92
 
71
- get (header) {
72
- return this.headers[header.toLowerCase()]
73
- }
93
+ get (header) { return this.headers[header.toLowerCase()] }
74
94
  }
75
95
 
76
- export function decorateRequest (uwsReq, pathParameters, res, config) {
77
- return new SpliffyRequest(uwsReq, pathParameters, res, config)
96
+ export function decorateRequest (uwsReq, paramToIndex, res, config) {
97
+ return new SpliffyRequest(uwsReq, paramToIndex, res, config)
78
98
  }
79
99
 
80
- export function decorateResponse (res, req, finalizeResponse, errorTransformer, endError, config) {
81
- res.onAborted(() => {
82
- res.ended = true
83
- res.writableEnded = true
84
- res.finalized = true
85
- log.error(`Request to ${req.url} was aborted`)
86
- })
87
- res.acceptsDefault = config.acceptsDefault
88
- res.headers = {}
89
- res.headersSent = false
90
- res.setHeader = (header, value) => {
91
- res.headers[header.toLowerCase()] = value
92
- }
93
- res.removeHeader = header => {
94
- delete res.headers[header.toLowerCase()]
95
- }
96
- res.flushHeaders = () => {
97
- if (res.headersSent) return
98
- if (!res.statusCode) res.statusCode = httpStatusCodes.OK
99
- if (!res.statusMessage) res.statusMessage = defaultStatusMessages[res.statusCode]
100
- res.headersSent = true
101
- res.writeStatus(`${res.statusCode} ${res.statusMessage}`)
102
- if (typeof res.onFlushHeaders === 'function') {
103
- res.onFlushHeaders(res)
104
- }
105
- for (const header in res.headers) {
106
- const val = res.headers[header]
107
- if (Array.isArray(val)) {
108
- for (let i = 0; i < val.length; i++) {
109
- res.writeHeader(header, val[i].toString())
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]))
136
+ } else {
137
+ this.writeHeader(h, String(v))
110
138
  }
111
- } else {
112
- res.writeHeader(header, val.toString())
113
139
  }
114
140
  }
115
- }
116
- res.writeHead = (status, headers) => {
117
- res.statusCode = status
118
- if (headers) res.assignHeaders(headers)
119
- }
120
- res.assignHeaders = headers => {
121
- for (const header in headers) {
122
- res.headers[header.toLowerCase()] = headers[header]
123
- }
124
- }
125
- res.getHeader = header => {
126
- return res.headers[header.toLowerCase()]
127
- }
128
- res.status = (code) => {
129
- res.statusCode = code
130
- return res
131
- }
132
-
133
- const uwsWrite = res.write
134
- res.write = (chunk, encoding, cb) => {
135
- res.cork(() => {
136
- res.streaming = true
137
- res.flushHeaders()
138
- let data
139
- if (chunk instanceof Buffer || chunk instanceof Uint8Array || typeof chunk === 'string') {
140
- data = chunk
141
- } else {
142
- data = JSON.stringify(chunk)
143
- }
144
- const result = uwsWrite.call(res, data)
145
- if (typeof cb === 'function') cb()
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()
146
156
  return result
147
157
  })
148
- }
149
-
150
- res.getWritable = () => {
151
- if (!res.outStream) {
152
- res.streaming = true
153
- res.outStream = new Writable({
154
- write: (chunk, encoding, callback) => {
155
- res.write(chunk, encoding, callback)
156
- }
157
- })
158
- .on('finish', () => res.end())
159
- .on('error', e => {
160
- try {
161
- res.outStream.destroy()
162
- } finally {
163
- endError(res, e, uuid(), errorTransformer)
164
- }
165
- })
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) } })
166
176
  }
167
- return res.outStream
177
+ return this.outStream
168
178
  }
179
+ }
169
180
 
170
- const uwsEnd = res.end
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
208
+
209
+ res._uwsEnd = res.end
171
210
  res.ended = false
172
- res.end = body => {
173
- if (res.ended) return
174
- res.cork(() => {
175
- if (!res.headersSent) res.flushHeaders()
176
- uwsEnd.call(res, body || '')
177
- res.writableEnded = true
178
- res.ended = true
179
- if (typeof res.onEnd === 'function') res.onEnd()
180
- })
181
- }
211
+ res.end = resProto.end
182
212
 
183
- res.redirect = (code, location) => {
184
- if (typeof code === 'string') {
185
- location = code
186
- code = httpStatusCodes.MOVED_PERMANENTLY
187
- }
188
- return finalizeResponse(req, res, {
189
- statusCode: code,
190
- headers: { location }
191
- })
192
- }
193
- res.send = (body) => finalizeResponse(req, res, body)
194
- res.json = res.send
195
- res.setCookie = (name, value, options) => {
196
- const serialized = cookie.serialize(name, value, options)
197
- const existing = res.getHeader('Set-Cookie')
198
- if (existing) {
199
- if (Array.isArray(existing)) existing.push(serialized)
200
- else res.setHeader('Set-Cookie', [existing, serialized])
201
- } else {
202
- res.setHeader('Set-Cookie', serialized)
203
- }
204
- }
205
- res.cookie = res.setCookie
206
213
  return res
207
214
  }
package/src/handler.mjs CHANGED
@@ -1,41 +1,28 @@
1
1
  import log from './log.mjs'
2
2
  import { deserializeBody, serializeBody } from './content.mjs'
3
3
  import { invokeMiddleware, preProcessMiddleware } from './middleware.mjs'
4
- import { decorateResponse, decorateRequest } from './decorator.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 handled = await handler({ url, bodyPromise, headers: req.headers, req, res })
34
- finalizeResponse(req, res, handled, handler.statusCodeOverride)
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) {
38
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
25
+ try { await executeMiddleware(middleware, req, res, errorTransformer, refId, e) } catch (me) { log.error(me) }
39
26
  }
40
27
  endError(res, e, refId, errorTransformer)
41
28
  }
@@ -50,55 +37,37 @@ const endError = (res, e, refId, errorTransformer) => {
50
37
  }
51
38
  res.headers['x-ref-id'] = refId
52
39
  const status = e.statusCode || 500
53
- if (status === 500) {
54
- log.error(e)
55
- }
40
+ if (status === 500) log.error(e)
56
41
  end(res, status, null, e.body || '')
57
42
  }
58
43
 
59
44
  const end = (res, defaultStatusCode, statusCodeOverride, body) => {
60
- // status set directly on res wins
61
45
  res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
46
+ if (currentDate) res.setHeader('Date', currentDate)
62
47
  if (body instanceof Readable || res.streaming) {
63
48
  res.streaming = true
64
- if (body instanceof Readable) {
65
- pipeResponse(res, body)
66
- }
67
- // handler is responsible for ending the response if they are streaming
49
+ if (body instanceof Readable) pipeResponse(res, body)
68
50
  } else {
69
- res.end(doSerializeBody(body, res) || '')
70
- }
71
- }
72
-
73
- const ipv6CompressRegex = /\b:?(?:0+:?){2,}/g
74
-
75
- const compressIpv6 = ip => ip && ip.includes(':') ? ip.replaceAll(ipv6CompressRegex, '::') : ip
76
-
77
- const writeAccess = function (req, res) {
78
- const start = Date.now()
79
- return () => {
80
- log.access(compressIpv6(req.remoteAddress), compressIpv6(res.proxiedRemoteAddress) || '', res.statusCode, req.method, req.url, Date.now() - start + 'ms')
51
+ if (typeof body === 'string' || !body || body instanceof Buffer) {
52
+ res.end(body || '')
53
+ } else {
54
+ res.end(doSerializeBody(body, res) || '')
55
+ }
81
56
  }
82
57
  }
83
58
 
84
59
  const finalizeResponse = (req, res, handled, statusCodeOverride) => {
85
- if (!res.finalized) {
86
- if (!handled) {
87
- // 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
88
- end(res, 200, statusCodeOverride)
89
- } else {
90
- // if the returned object has known fields, treat it as a response object instead of the body
91
- if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
92
- if (handled.headers) {
93
- res.assignHeaders(handled.headers)
94
- }
95
- res.statusMessage = handled.statusMessage || res.statusMessage
96
- end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
97
- } else {
98
- end(res, 200, statusCodeOverride, handled)
99
- }
100
- }
101
- 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)
102
71
  }
103
72
  }
104
73
 
@@ -106,21 +75,13 @@ const pipeResponse = (res, readStream, errorTransformer) => {
106
75
  readStream.on('data', chunk => res.write(chunk))
107
76
  .on('end', () => res.end())
108
77
  .on('error', e => {
109
- try {
110
- readStream.destroy()
111
- } finally {
112
- endError(res, e, uuid(), errorTransformer)
113
- }
78
+ try { readStream.destroy() } finally { endError(res, e, uuid(), errorTransformer) }
114
79
  })
115
80
  }
116
81
 
117
82
  const doSerializeBody = (body, res) => {
118
- if (!body || typeof body === 'string' || body instanceof Readable) {
119
- return body
120
- }
121
- const contentType = res.getHeader('content-type')
83
+ const contentType = res.headers['content-type']
122
84
  const serialized = serializeBody(body, contentType, res.acceptsDefault)
123
-
124
85
  if (serialized?.contentType && !contentType) {
125
86
  res.headers['content-type'] = serialized.contentType
126
87
  }
@@ -129,10 +90,8 @@ const doSerializeBody = (body, res) => {
129
90
 
130
91
  async function executeMiddleware (processedMiddleware, req, res, errorTransformer, refId, e) {
131
92
  if (!processedMiddleware) return
132
-
133
93
  const methodMiddleware = processedMiddleware[req.method]
134
94
  const allMiddleware = processedMiddleware.ALL
135
-
136
95
  if (e) {
137
96
  if (allMiddleware?.error) await invokeMiddleware(allMiddleware.error, req, res, e)
138
97
  if (methodMiddleware?.error) await invokeMiddleware(methodMiddleware.error, req, res, e)
@@ -142,80 +101,96 @@ async function executeMiddleware (processedMiddleware, req, res, errorTransforme
142
101
  }
143
102
  }
144
103
 
145
- const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
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) => {
146
110
  try {
147
- let reqBody
148
- if (!handler.streamRequestBody) {
149
- if (req.method === 'GET' || req.method === 'HEAD') {
150
- reqBody = Promise.resolve(null)
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
+ })
151
122
  } else {
152
- let buffer
153
- reqBody = new Promise(
154
- resolve =>
155
- res.onData((data, isLast) => {
156
- const chunk = Buffer.concat([Buffer.from(data)])
157
- buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk
158
- if (isLast) {
159
- resolve(buffer)
160
- }
161
- })
162
- )
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
+ }))
163
128
  }
164
- } else {
165
- const readable = new Readable({
166
- read: () => {
167
- }
168
- })
169
- res.onData((data, isLast) => {
170
- if (data.byteLength > 0 || isLast) {
171
- readable.push(Buffer.concat([Buffer.from(data)]))
172
- if (isLast) {
173
- readable.push(null)
174
- }
175
- }
176
- })
177
- reqBody = Promise.resolve(readable)
178
- }
179
- if (middleware) {
180
- await executeMiddleware(middleware, req, res, errorTransformer)
181
129
  }
182
- if (!res.writableEnded && !res.ended) {
183
- await executeHandler(req.spliffyUrl, res, req, reqBody, handler, middleware, errorTransformer)
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)
184
138
  }
185
139
  } catch (e) {
186
140
  const refId = uuid()
187
- if (middleware) {
188
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
141
+ if (processedMiddleware) {
142
+ await executeMiddleware(processedMiddleware, sReq, res, config.errorTransformer, refId, e)
189
143
  }
190
- if (!res.writableEnded) { endError(res, e, refId, errorTransformer) }
144
+ if (!res.writableEnded) { endError(res, e, refId, config.errorTransformer) }
191
145
  }
192
146
  }
193
147
 
194
- export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
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
+ }
195
154
 
196
- let currentDate = new Date().toISOString()
197
- setInterval(() => { currentDate = new Date().toISOString() }, 1000)
155
+ const isWildcardPath = urlPath?.indexOf('*') > -1
156
+
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
- export const createHandler = (handler, middleware, pathParameters, config) => {
200
- const processedMiddleware = preProcessMiddleware(middleware)
201
167
  return function (res, req) {
202
168
  try {
203
- res.cork(() => {
204
- req = decorateRequest(req, pathParameters, res, config)
205
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
206
-
207
- if (config.logAccess) {
208
- // pre-fetch IPs for logging while still safe
209
- req._ra = req.remoteAddress
210
- req._pra = req.proxiedRemoteAddress
211
- res.onEnd = writeAccess(req, res)
212
- }
213
-
214
- if (config.writeDateHeader) {
215
- res.headers.date = currentDate
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)
216
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()
217
192
 
218
- handleRequest(req, res, handler, processedMiddleware, config.errorTransformer)
193
+ handleRequest(sReq, res, handler, processedMiddleware, config)
219
194
  .catch(e => {
220
195
  log.error('Failed handling request', e)
221
196
  if (!res.writableEnded) {
@@ -223,7 +198,7 @@ export const createHandler = (handler, middleware, pathParameters, config) => {
223
198
  res.end()
224
199
  }
225
200
  })
226
- })
201
+ }
227
202
  } catch (e) {
228
203
  log.error('Failed handling request', e)
229
204
  if (!res.writableEnded) {
@@ -236,44 +211,36 @@ export const createHandler = (handler, middleware, pathParameters, config) => {
236
211
 
237
212
  export const createNotFoundHandler = config => {
238
213
  const handler = config.defaultRouteHandler || config.notFoundRouteHandler
239
- const params = handler?.pathParameters || []
214
+ const paramToIndex = handler?.paramToIndex || {}
240
215
  return (res, req) => {
241
216
  try {
242
- res.cork(() => {
243
- req = decorateRequest(req, params, res, config)
244
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
245
- if (config.logAccess) {
246
- req._ra = req.remoteAddress
247
- req._pra = req.proxiedRemoteAddress
248
- res.onEnd = writeAccess(req, res)
249
- }
250
- if (handler && typeof handler === 'object') {
251
- const processedMiddleware = preProcessMiddleware(handler.middleware)
252
- if (handler.handlers && typeof handler.handlers[req.method] === 'function') {
253
- const h = handler.handlers[req.method]
254
- if ('statusCodeOverride' in handler) {
255
- h.statusCodeOverride = handler.statusCodeOverride
256
- }
257
- handleRequest(req, res,
258
- h,
259
- processedMiddleware,
260
- config.errorTransformer
261
- ).catch((e) => {
217
+ const sReq = new SpliffyRequest(req, paramToIndex, res, config)
218
+ sReq.forceCache()
219
+ decorateResponse(res, sReq, finalizeResponse, config.errorTransformer, endError, config)
220
+ res.ensureOnAborted()
221
+ if (handler && typeof handler === 'object') {
222
+ const processedMiddleware = preProcessMiddleware(handler.middleware)
223
+ if (handler.handlers && typeof handler.handlers[sReq.method] === 'function') {
224
+ const h = handler.handlers[sReq.method]
225
+ if ('statusCodeOverride' in handler) {
226
+ h.statusCodeOverride = handler.statusCodeOverride
227
+ }
228
+ handleRequest(sReq, res, h, processedMiddleware, config)
229
+ .catch(e => {
262
230
  log.error('Unexpected exception during request handling', e)
263
231
  if (!res.writableEnded) {
264
232
  res.statusCode = 500
265
233
  res.end()
266
234
  }
267
235
  })
268
- } else {
269
- res.statusCode = 404
270
- res.end()
271
- }
272
236
  } else {
273
237
  res.statusCode = 404
274
238
  res.end()
275
239
  }
276
- })
240
+ } else {
241
+ res.statusCode = 404
242
+ res.end()
243
+ }
277
244
  } catch (e) {
278
245
  log.error('Failed handling request', e)
279
246
  }
package/src/index.mjs CHANGED
@@ -29,3 +29,5 @@ export { default } from './start.mjs'
29
29
  * @return {string} The full path to the directory the module is in
30
30
  */
31
31
  export const moduleDirname = metaUrl => path.dirname(fileURLToPath(metaUrl))
32
+
33
+ export * from './errors.mjs'
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.pathParameters, config)
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) {