@srfnstack/spliffy 0.5.5 → 0.8.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.
@@ -0,0 +1,84 @@
1
+ import { initContentHandlers } from './content.mjs'
2
+ import { validateMiddleware } from './middleware.mjs'
3
+ import log from './log.mjs'
4
+
5
+ const defaultHeaders = {
6
+ acceptsDefault: '*/*',
7
+ defaultContentType: '*/*'
8
+ }
9
+ // this is mainly for performance reasons
10
+ const nonsense = [
11
+ 'I\'m toasted',
12
+ 'that hurt',
13
+ 'your interwebs!',
14
+ 'I see a light...',
15
+ 'totally zooted',
16
+ 'misplaced my bits',
17
+ 'maybe reboot?',
18
+ 'what was I doing again?',
19
+ 'my cabbages!!!',
20
+ 'Leeerrroooyyy Jeeenkins',
21
+ 'at least I have chicken'
22
+ ]
23
+
24
+ export const randomNonsense = () => `[OH NO, ${nonsense[Math.floor(Math.random() * nonsense.length)]}]`
25
+
26
+ export async function initConfig (userConfig) {
27
+ const config = Object.assign({}, userConfig)
28
+
29
+ if (!('decodePathParameters' in config)) {
30
+ config.decodePathParameters = true
31
+ }
32
+
33
+ if (!('parseCookie' in config)) {
34
+ config.parseCookie = true
35
+ }
36
+
37
+ config.acceptsDefault = config.acceptsDefault || defaultHeaders.acceptsDefault
38
+ config.defaultContentType = config.defaultContentType || defaultHeaders.defaultContentType
39
+
40
+ config.contentHandlers = initContentHandlers(config.contentHandlers || {})
41
+ config.resolveWithoutExtension = config.resolveWithoutExtension || []
42
+ if (!Array.isArray(config.resolveWithoutExtension)) {
43
+ config.resolveWithoutExtension = [config.resolveWithoutExtension]
44
+ }
45
+
46
+ if (config.resolveWithoutExtension.indexOf('.htm') === -1) {
47
+ config.resolveWithoutExtension.push('.htm')
48
+ }
49
+ if (config.resolveWithoutExtension.indexOf('.html') === -1) {
50
+ config.resolveWithoutExtension.push('.html')
51
+ }
52
+
53
+ if (config.middleware) {
54
+ validateMiddleware(config.middleware)
55
+ }
56
+
57
+ if (!('logAccess' in config)) {
58
+ config.logAccess = true
59
+ }
60
+ if ('logLevel' in config) {
61
+ log.setLogLevel(config.logLevel)
62
+ }
63
+ if (!('ignoreFilesMatching' in config)) {
64
+ config.ignoreFilesMatching = []
65
+ } else if (!Array.isArray(config.ignoreFilesMatching)) {
66
+ config.ignoreFilesMatching = [config.ignoreFilesMatching]
67
+ }
68
+ if (!('allowTestFileRoutes' in config)) {
69
+ config.allowTestFileRoutes = false
70
+ }
71
+ config.port = config.port || 10420
72
+ if (!config.httpPort) {
73
+ config.httpPort = config.port - 1
74
+ }
75
+
76
+ if (config.logger) {
77
+ log.setLogger(config.logger)
78
+ }
79
+
80
+ if ((config.httpsKeyFile && !config.httpsCertFile) || (!config.httpsKeyFile && config.httpsCertFile)) {
81
+ throw new Error('You must provide both httpsKeyFile and httpsCertFile')
82
+ }
83
+ return config
84
+ }
package/src/start.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { initConfig, randomNonsense } from './serverConfig.mjs'
2
+ import log from './log.mjs'
3
+ import { startServer } from './server.mjs'
4
+
5
+ export default async function (config) {
6
+ if (!config || !config.routeDir) {
7
+ throw new Error('You must supply a config object with at least a routeDir property. routeDir should be a full path.')
8
+ }
9
+ process
10
+ .on('unhandledRejection', (reason, p) => {
11
+ log.error(randomNonsense(), reason, 'Unhandled Rejection at Promise', p)
12
+ })
13
+ .on('uncaughtException', (err, origin) => {
14
+ log.error(randomNonsense(), `Caught unhandled exception: ${err}\n` +
15
+ `Exception origin: ${origin}`)
16
+ })
17
+
18
+ log.gne('Starting Spliffy!')
19
+ const configWithDefaults = await initConfig(config)
20
+ return startServer(configWithDefaults).catch(e => {
21
+ log.error(randomNonsense(), 'Exception during startup:', e)
22
+ // Spliffy threw an exception, or a route handler failed to load.
23
+ process.exit(420)
24
+ })
25
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'fs'
2
+ import etag from 'etag'
3
+
4
+ const readFile = async (fullPath) => await new Promise(
5
+ (resolve, reject) =>
6
+ fs.readFile(fullPath, (err, data) => {
7
+ if (err) reject(err)
8
+ else resolve(data)
9
+ }
10
+ )
11
+ )
12
+
13
+ const writeHeaders = (req, res, tag, stat, contentType, staticCacheControl) => {
14
+ if (req.headers['if-none-match'] === tag) {
15
+ res.statusCode = 304
16
+ return
17
+ }
18
+ res.writeHead(200, {
19
+ 'Content-Type': contentType,
20
+ // content-length should not be included because transfer-encoding is chunked
21
+ // see https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 sub section 3.
22
+ // Not all clients are compliant (node-fetch) and throw instead of ignoring the header as specified
23
+ 'Cache-Control': staticCacheControl || 'max-age=600',
24
+ ETag: tag
25
+ })
26
+ }
27
+
28
+ const readStat = async path => new Promise((resolve, reject) =>
29
+ fs.stat(path, (err, stats) =>
30
+ err ? reject(err) : resolve(stats)
31
+ ))
32
+
33
+ export function createStaticHandler (fullPath, contentType, cacheStatic, staticCacheControl) {
34
+ const cache = {}
35
+ return {
36
+ GET: async ({ req, res }) => {
37
+ if (cacheStatic) {
38
+ if (!cache.exists || !cache.stat) {
39
+ cache.exists = fs.existsSync(fullPath)
40
+ if (cache.exists) {
41
+ cache.stat = await readStat(fullPath)
42
+ cache.content = await readFile(fullPath)
43
+ cache.etag = etag(cache.content)
44
+ }
45
+ }
46
+ if (!cache.exists) {
47
+ return {
48
+ statusCode: 404
49
+ }
50
+ }
51
+ writeHeaders(req, res, cache.etag, cache.stat, contentType, staticCacheControl)
52
+ if (res.statusCode === 304) {
53
+ return
54
+ }
55
+ return cache.content
56
+ } else {
57
+ if (!fs.existsSync(fullPath)) {
58
+ return {
59
+ statusCode: 404
60
+ }
61
+ }
62
+ const stat = await readStat(fullPath)
63
+ writeHeaders(req, res, etag(stat), stat, contentType, staticCacheControl)
64
+ if (res.statusCode === 304) {
65
+ return
66
+ }
67
+ if (stat.size === 0) {
68
+ return ''
69
+ }
70
+ return fs.createReadStream(fullPath)
71
+ }
72
+ }
73
+ }
74
+ }
package/src/url.mjs ADDED
@@ -0,0 +1,24 @@
1
+ export function parseQuery (query, decodeQueryParams) {
2
+ const parsed = {}
3
+ if (query) {
4
+ if (decodeQueryParams) {
5
+ query = decodeURIComponent(query.replace(/\+/g, '%20'))
6
+ }
7
+ for (const param of query.split('&')) {
8
+ const eq = param.indexOf('=')
9
+ setMultiValueKey(parsed, param.substr(0, eq), param.substr(eq + 1))
10
+ }
11
+ }
12
+ return parsed
13
+ }
14
+
15
+ export function setMultiValueKey (obj, key, value) {
16
+ if (key in obj) {
17
+ if (!Array.isArray(obj[key])) {
18
+ obj[key] = [obj[key]]
19
+ }
20
+ obj[key].push(value)
21
+ } else {
22
+ obj[key] = value
23
+ }
24
+ }
@@ -1,75 +0,0 @@
1
- module.exports = {
2
- '.aac': 'audio/aac',
3
- '.abw': 'application/x-abiword',
4
- '.arc': 'application/x-freearc',
5
- '.avi': 'video/x-msvideo',
6
- '.azw': 'application/vnd.amazon.ebook',
7
- '.bin': 'application/octet-stream',
8
- '.bmp': 'image/bmp',
9
- '.bz': 'application/x-bzip',
10
- '.bz2': 'application/x-bzip2',
11
- '.csh': 'application/x-csh',
12
- '.css': 'text/css',
13
- '.csv': 'text/csv',
14
- '.doc': 'application/msword',
15
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
16
- '.eot': 'application/vnd.ms-fontobject',
17
- '.epub': 'application/epub+zip',
18
- '.gif': 'image/gif',
19
- '.htm': 'text/html',
20
- '.html': 'text/html',
21
- '.ico': 'image/vnd.microsoft.icon',
22
- '.ics': 'text/calendar',
23
- '.jar': 'application/java-archive',
24
- '.jpeg': 'image/jpeg',
25
- '.jpg': 'image/jpeg',
26
- '.js': 'text/javascript',
27
- '.json': 'application/json',
28
- '.jsonld': 'application/ld+json',
29
- '.mid': 'audio/midi',
30
- '.midi': 'audio/x-midi',
31
- '.mjs': 'text/javascript',
32
- '.mp3': 'audio/mpeg',
33
- '.mpeg': 'video/mpeg',
34
- '.mpkg': 'application/vnd.apple.installer+xml',
35
- '.odp': 'application/vnd.oasis.opendocument.presentation',
36
- '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
37
- '.odt': 'application/vnd.oasis.opendocument.text',
38
- '.oga': 'audio/ogg',
39
- '.ogv': 'video/ogg',
40
- '.ogg': 'application/ogg',
41
- '.ogx': 'application/ogg',
42
- '.otf': 'font/otf',
43
- '.png': 'image/png',
44
- '.pdf': 'application/pdf',
45
- '.ppt': 'application/vnd.ms-powerpoint',
46
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
47
- '.rar': 'application/x-rar-compressed',
48
- '.rtf': 'application/rtf',
49
- '.sh': 'application/x-sh',
50
- '.svg': 'image/svg+xml',
51
- '.swf': 'application/x-shockwave-flash',
52
- '.tar': 'application/x-tar',
53
- '.tif': 'image/tiff',
54
- '.tiff': 'font/ttf',
55
- '.ts': 'video/mp2t',
56
- '.ttf': 'font/ttf ',
57
- '.txt': 'text/plain',
58
- '.vsd': 'application/vnd.visio',
59
- '.wav': 'audio/wav',
60
- '.weba': 'audio/webm',
61
- '.webm': 'video/webm',
62
- '.webp': 'image/webp',
63
- '.woff': 'font/woff',
64
- '.woff2': 'font/woff2',
65
- '.xhtml': 'application/xhtml+xml',
66
- '.xls': 'application/vnd.ms-excel',
67
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
68
- '.xml': 'application/xml',
69
- '.xul': 'application/vnd.mozilla.xul+xml',
70
- '.zip': 'application/zip',
71
- '.3gp': 'video/3gpp',
72
- '.3g2': 'video/3gpp2',
73
- '.7z': 'application/x-7z-compressed ',
74
- 'default': 'application/octet-stream'
75
- }
package/src/content.js DELETED
@@ -1,56 +0,0 @@
1
- let defaultHandler = {
2
- deserialize: o => {
3
- try {
4
- return JSON.parse( typeof o === 'string' ? o : String( o ) )
5
- } catch( e ) {
6
- return o
7
- }
8
- },
9
- serialize: o => {
10
- if( o instanceof Buffer || typeof o === 'string' ) {
11
- return o
12
- }
13
- return {
14
- contentType: 'application/json',
15
- data: JSON.stringify( typeof o === 'object' ? o : o && o.toString() )
16
- }
17
- }
18
- };
19
- const contentHandlers = {
20
- 'application/json': {
21
- deserialize: s => JSON.parse( typeof s === 'string' ? s : String( s ) ),
22
- serialize: s => JSON.stringify( s )
23
- },
24
- 'application/octet-stream': defaultHandler,
25
- '*/*': defaultHandler
26
- }
27
-
28
- let _acceptsDefault = '*/*'
29
-
30
- function getHandler( contentType ) {
31
- //content-type should be singular https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.17
32
- let handler = contentHandlers[contentType];
33
- if( handler && typeof handler ) {
34
- if( typeof handler.serialize !== 'function' ) {
35
- throw new Error( `Content handlers must provide a serialize function. ${handler}` )
36
- }
37
- if( typeof handler.deserialize !== 'function' ) {
38
- throw new Error( `Content handlers must provide a deserialize function. ${handler}` )
39
- }
40
- return handler
41
- }
42
- return contentHandlers[_acceptsDefault]
43
- }
44
-
45
- module.exports = {
46
- serialize( content, contentType ) {
47
- return getHandler( contentType ).serialize( content )
48
- },
49
- deserialize( content, contentType ) {
50
- return getHandler( contentType ).deserialize( content )
51
- },
52
- initContentHandlers( handlers, acceptsDefault ) {
53
- Object.assign( handlers, contentHandlers )
54
- _acceptsDefault = acceptsDefault
55
- }
56
- }
package/src/dispatcher.js DELETED
@@ -1,190 +0,0 @@
1
- const log = require( './log' )
2
- const serverConfig = require( './serverConfig' )
3
- const routes = require( './routes' )
4
- const content = require( './content' )
5
- const { executeMiddleware } = require( "./middleware" );
6
- const { decorateResponse } = require( './expressShim.js' )
7
- const { decorateRequest } = require( './expressShim.js' )
8
- const { HTTP_METHODS } = require( './routes.js' )
9
- const parseUrl = require( './parseUrl' )
10
- const uuid = require( 'uuid' ).v4
11
-
12
- /**
13
- * Actually handle an incoming request
14
- * @param url The url being requested
15
- * @param res The node response object
16
- * @param req The node request object
17
- * @param body The request body
18
- * @param handler The handler for the route
19
- * @param middleware The middleware that applies to this request
20
- */
21
- const handle = async ( url, res, req, body, handler, middleware ) => {
22
- try {
23
- if( body )
24
- body = content.deserialize( body, req.headers['content-type'] )
25
- } catch( e ) {
26
- log.error( 'Failed to parse request.', e )
27
- end( res, 400, 'Failed to parse request body' )
28
- return
29
- }
30
-
31
- try {
32
- let handled = handler[req.method](
33
- {
34
- url,
35
- body,
36
- headers: req.headers,
37
- req,
38
- res
39
- } )
40
- if( handled && handled.then && typeof handled.then == 'function' ) {
41
- await handled.then( ( h ) => finalizeResponse( req, res, h ) )
42
- .catch(
43
- e => {
44
- let refId = uuid()
45
- log.error( e, refId )
46
- handleError( res, e, refId )
47
- }
48
- )
49
- } else {
50
- finalizeResponse( req, res, handled )
51
- }
52
-
53
- } catch( e ) {
54
- let refId = uuid()
55
- log.error( 'handler failed', e, refId )
56
- await tryExecuteMiddleware( middleware, req, res, e, refId )
57
- handleError( res, e, refId )
58
- }
59
- }
60
-
61
- const handleError = ( res, e, refId ) => {
62
- if( e.body && typeof e.body !== 'string' ) {
63
- e.body = JSON.stringify( e.body )
64
- }
65
- if( serverConfig.current.errorTransformer ) {
66
- e = serverConfig.current.errorTransformer( e, refId )
67
- }
68
- end( res, e.statusCode || e.status || 500, ( e.statusMessage || e.message || 'Internal Server Error' ) + ' refId: ' + refId, e.body || '' )
69
- }
70
-
71
- const end = ( res, code, message, body ) => {
72
- res.statusCode = code
73
- res.statusMessage = message
74
- res.end( body || '' )
75
- }
76
-
77
- const logAccess = function( req, res ) {
78
- const start = new Date().getTime()
79
- return () => {
80
- log.access( req.remoteAddress, res.proxiedRemoteAddress || '', res.statusCode, req.method, req.url, new Date().getTime() - start + 'ms' )
81
- }
82
- }
83
-
84
- const finalizeResponse = ( req, res, handled ) => {
85
- if( !res.ended ) {
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, 'OK' )
89
- } else {
90
- let code = 200
91
- let message = 'OK'
92
- let body = handled
93
- if( handled.body || handled.status || handled.statusMessage || handled.statusCode || handled.headers ) {
94
- body = handled.body
95
- if( handled.headers ) {
96
- res.assignHeaders( handled.headers )
97
- }
98
- code = handled.statusCode || 200
99
- message = handled.statusMessage || 'OK'
100
- }
101
- let contentType = res.getHeader( 'content-type' )
102
-
103
- let serialized = content.serialize( body, contentType );
104
- if( serialized && serialized.contentType && !contentType ) {
105
- res.setHeader( 'content-type', serialized.contentType )
106
- }
107
- end( res, code, message, serialized && serialized.data || serialized )
108
- }
109
- } else {
110
- log.warn( `Tried to finalize ended response for req: ${req.url}` )
111
- }
112
- }
113
-
114
- async function tryExecuteMiddleware( middleware, req, res, e, refId ) {
115
- if( !middleware ) return
116
- try {
117
- if( e )
118
- await executeMiddleware( middleware, req, res, e )
119
- else
120
- await executeMiddleware( middleware, req, res )
121
- } catch( e ) {
122
- if( !refId ) refId = uuid()
123
- console.log( 'Failed executing middleware while handling error: ' + e )
124
- handleError( res, e, refId )
125
- }
126
- }
127
-
128
- let currentDate = new Date().toUTCString()
129
- setInterval( () => currentDate = new Date().toUTCString(), 1000 )
130
-
131
- const handleRequest = async ( req, res ) => {
132
- req = decorateRequest( req, res )
133
- res = decorateResponse( res, req, finalizeResponse )
134
-
135
- res.setHeader( 'server', 'uws' )
136
- res.setHeader( 'date', currentDate )
137
-
138
- let route = routes.find( req.spliffyUrl )
139
- if( !route.handler && serverConfig.current.notFoundRoute ) {
140
- route = routes.find( parseUrl( ...serverConfig.current.notFoundRoute.split( '?' ) ) )
141
- }
142
- if( !route.handler ) {
143
- end( res, 404, 'Not Found' )
144
- } else if( req.method === 'OPTIONS' || ( route.handler && route.handler[req.method] ) ) {
145
- try {
146
- let buffer
147
-
148
- let reqBody = await new Promise(
149
- resolve =>
150
- res.onData( async ( data, isLast ) => {
151
- if( isLast ) {
152
- buffer = data.byteLength > 0 ? Buffer.concat( [buffer, Buffer.from( data )].filter( b => b ) ) : buffer
153
- resolve( buffer )
154
- }
155
- buffer = Buffer.concat( [buffer, Buffer.from( data )].filter( b => b ) )
156
- } )
157
- )
158
- req.spliffyUrl.pathParameters = route.pathParameters
159
- await tryExecuteMiddleware( route.middleware, req, res )
160
- if( !res.writableEnded ) {
161
- if( req.method === 'OPTIONS' && !route.handler.OPTIONS ) {
162
- res.setHeader( 'Allow', Object.keys( route.handler ).filter( key => HTTP_METHODS.indexOf( key ) > -1 ).join( ', ' ) )
163
- end( res, 204 )
164
- } else {
165
- await handle( req.spliffyUrl, res, req, reqBody, route.handler, route.middleware )
166
- }
167
- }
168
- } catch( e ) {
169
- let refId = uuid()
170
- log.error( 'Handling request failed', e, refId )
171
- await tryExecuteMiddleware( route.middleware, req, res, e, refId );
172
- if( !res.writableEnded )
173
- handleError( res, e, refId )
174
- }
175
- } else {
176
- end( res, 405, 'Method Not Allowed' )
177
- }
178
- }
179
-
180
- module.exports =
181
- function( res, req ) {
182
- try {
183
- if( serverConfig.current.logAccess ) {
184
- res.onEnd = logAccess( req, res )
185
- }
186
- handleRequest( req, res )
187
- } catch( e ) {
188
- log.error( 'Failed handling request', e )
189
- }
190
- }
@@ -1,120 +0,0 @@
1
- const cookie = require( 'cookie' )
2
- const serverConfig = require( './serverConfig' )
3
- const http = require('http')
4
- const parseUrl = require( './parseUrl' )
5
- const setCookie = ( res ) => function() {
6
- return res.setHeader( 'set-cookie', [...( res.getHeader( 'set-cookie' ) || [] ), cookie.serialize( ...arguments )] )
7
- }
8
- const log = require( './log' )
9
- const addressArrayBufferToString = addrBuf => String.fromCharCode.apply( null, new Int8Array( addrBuf ) )
10
- const excludedMessageProps = {
11
- setTimeout: true, _read: true, destroy: true, _addHeaderLines: true, _addHeaderLine: true, _dump: true, __proto__: true
12
- }
13
-
14
- /**
15
- * Provide a minimal set of shims to make most middleware, like passport, work
16
- * @type {{decorateResponse(*=, *=, *): *, decorateRequest(*): *}}
17
- */
18
- module.exports = {
19
- setCookie,
20
- decorateRequest( req, res ) {
21
- let query = req.getQuery()
22
- req.url = `${req.getUrl()}${query ? '?' + query : ''}`
23
- req.spliffyUrl = parseUrl( req.getUrl(), req.getQuery() )
24
- req.headers = {}
25
- req.method = req.getMethod().toUpperCase()
26
- req.remoteAddress = addressArrayBufferToString( res.getRemoteAddressAsText() )
27
- req.proxiedRemoteAddress = addressArrayBufferToString( res.getProxiedRemoteAddressAsText() ) || undefined
28
- req.forEach( ( header, value ) => req.headers[header] = value )
29
- if( serverConfig.current.parseCookie && req.headers.cookie ) {
30
- req.cookies = cookie.parse( req.headers.cookie ) || {}
31
- }
32
- //frameworks like passport like to modify the message prototype...
33
- for(let p of Object.keys(http.IncomingMessage.prototype)){
34
- if(!req[p] && !excludedMessageProps[p]) req[p] = http.IncomingMessage.prototype[p]
35
- }
36
- return req
37
- },
38
- decorateResponse( res, req, finalizeResponse ) {
39
- res.onAborted( () => {
40
- log.error( `Request to ${req.url} was aborted prematurely` )
41
- } )
42
- const writeHead = {}
43
-
44
- res.headers = {}
45
- res.headersSent = false
46
- res.setHeader = ( header, value ) => {
47
- res.headers[header.toLowerCase()] = value
48
- }
49
- res.removeHeader = header => delete res.headers[header.toLowerCase()]
50
- res.flushHeaders = () => {
51
- res.headersSent = true
52
- // https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
53
- //When headers have been set with response.setHeader(), they will be merged with any headers passed to response.writeHead(), with the headers passed to response.writeHead() given precedence.
54
- Object.assign( res.headers, writeHead )
55
- for( let header of Object.keys( res.headers ) ) {
56
- if( Array.isArray( res.headers[header] ) ) {
57
- for( let multiple of res.headers[header] ) {
58
- res.writeHeader( header, multiple.toString() )
59
- }
60
- } else {
61
- res.writeHeader( header, res.headers[header].toString() )
62
- }
63
- }
64
- }
65
- res.writeHead = ( status, headers ) => {
66
- this.statusCode = status
67
- res.assignHeaders( headers )
68
- }
69
- res.assignHeaders = headers => {
70
- for( let header of Object.keys( headers ) ) {
71
- writeHead[header.toLowerCase()] = headers[header]
72
- }
73
- }
74
- res.getHeader = header => {
75
- let lc = header.toLowerCase()
76
- return writeHead[lc] || res.headers[lc]
77
- }
78
- res.status = ( code ) => {
79
- this.statusCode = code
80
- return this
81
- }
82
-
83
- const ogEnd = res.end
84
- res.ended = false
85
- res.end = body => {
86
- if( res.ended ) return
87
- res.ended = true
88
- if(!res.writableEnded){
89
- res.writableEnded = true
90
- res.writeStatus( `${res.statusCode} ${res.statusMessage}` )
91
- res.flushHeaders()
92
- ogEnd.call( res, body )
93
- }
94
- if(typeof res.onEnd === 'function'){
95
- res.onEnd()
96
- }
97
- }
98
-
99
- res.redirect = function( code, location ) {
100
- if( arguments.length === 1 ) {
101
- location = code
102
- code = 301
103
- }
104
- return finalizeResponse( req, res, {
105
- statusCode: code,
106
- headers: {
107
- 'location': location
108
- }
109
- } )
110
- }
111
- res.send = ( body ) => {
112
- finalizeResponse( req, res, body )
113
- }
114
- res.json = res.send
115
- res.setCookie = setCookie( res )
116
- res.cookie = res.setCookie
117
- return res
118
-
119
- }
120
- }