├── .gitignore ├── .travis.yml ├── ChangeLog ├── LICENSE ├── Readme.md ├── benchmark └── benchmark.js ├── http-client.js ├── index.js ├── lib ├── errors.js ├── qrestify.js ├── qroute.js ├── restiq.js └── rlib.js ├── package.json └── test ├── quicktest1.js ├── sample.js ├── test-errors.js ├── test-package.js ├── test-qroute.js ├── test-restify.js ├── test-restiq.js └── test-rlib.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.nyc_output/ 3 | /coverage/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 5 5 | - 6 6 | - 8 7 | after_success: 8 | - if [ `node -p 'process.version.slice(0, 3)'` != "v8." ]; then exit; fi 9 | - npm install -g nyc 10 | - npm install -g codecov 11 | - nyc --reporter lcov -r text npm test && codecov 12 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 0.9.2 2 | - use the nodejs built-in querystring for encoding x-www-form-urlencoded responses 3 | - upgrade qhttp for fixed http_parse_query() (decode + into ' ' space) 4 | 5 | 0.9.1 6 | - fix res.send() to set res._body to raw object, and set res.headers 7 | - fix res.json() to set _body 8 | 9 | 0.9.0 10 | - process requests async for a large bump in throughput 11 | 12 | 0.8.1 13 | - fix 0 'use' steps before the mapped route 14 | - make removeRoute accept routes from both mapRoute() and addRoute() 15 | 16 | 0.8.0 17 | - fix restiq.error.Error* to construct proper Errors 18 | - refactor middleware runner to always call the _finally steps 19 | 20 | 0.7.6 21 | - restify compat: emit 'uncaughtException' on unhandled mw error 22 | - restify compat: experimental restiq.InvalidCredentialsError 23 | 24 | 0.7.5 25 | - check that the middleware error.code is a plausible 3 digit http status before using it 26 | - upgrade to qhttp-0.6.0 27 | - document setErrorHandler 28 | - let addStep(func, where) accept its args in either order 29 | 30 | 0.7.4 31 | - apply restify emulation before the route is mapped, to have res.send available in unit tests 32 | 33 | 0.7.3 34 | - only set the restify error handler once at start, to allow caller to override 35 | - restify compat: not a restify error if parseBody cannot decode body (with mapParams:false) 36 | - fix parseBody to set to decoded if it decodes to falsy 37 | 38 | 0.7.2 39 | - add to ci and coverage tests 40 | - test with qnit 0.15.1 with fixed failure exitcode 41 | - fix typo in makeError source string 42 | - use newer qmock 43 | - update dependencies (no need for qmock, yes for querystring) 44 | - guard against possible null deref in rlib 45 | 46 | 0.7.1 47 | - faster readBody() data chunk concatenation 48 | - list qmock as a dependency (been one since 2015) 49 | - fix: always return a Buffer in readBinary mode 50 | - fix: parseAuthorization mw should run ok without a callback 51 | 52 | 0.7.0 53 | - middleware builder functions 54 | - maxBodySize readBody and parseReadBodyParams option 55 | 56 | 0.6.3 57 | - restify compat: provide res._body 58 | - restify compat: suppress errors from `after` 59 | - run tests with qnit 60 | - upgrade aflow to 0.10.1 for speedup 61 | 62 | 0.6.2 63 | - fix potential null deref on unmapped routes 64 | - new .gitignore 65 | - fix and clean up Readme, thanks @fidian! 66 | 67 | 0.6.1 68 | - add req.path() compat method 69 | - tolerate double-close 70 | - read request as 'utf8' to not split multi-byte chars 71 | 72 | 0.6.0 73 | - bump to aflow 0.9.3 (unit tests, track latest) 74 | - bump to qhttp 0.0.6 (track latest) 75 | - fix Restiq.listen() callback to only call once started 76 | - remove qhttp sources 77 | - make Restiq() build apps like express() does 78 | 79 | 0.5.3 80 | - move http-client into the qhttp package 81 | - change arlib dependency to qhttp 82 | 83 | 0.5.2 84 | - split http-client out of test-restiq 85 | 86 | 0.5.1 87 | - speedup: improve readBody readBinary codepath, redo timings 88 | 89 | 0.5.0 90 | - app.removeRoute method 91 | - [options] param to addRoute (for later) 92 | - refactor app routers into a struct 93 | 94 | 0.4.3 95 | - bugfix: decodeBody should not change req.body if cannot decode 96 | - compat: make decodeQueryParams populate req.query as well as req.params 97 | - bugfix: propagate errors from parseBody() 98 | 99 | 0.4.2 100 | - switch to querystring.encode() for now for compatible flat array encodings 101 | - bump to arlib 0.2.4 to pick up http_parse_query fixes 102 | - revert slower array-of-chunks readBinary, toString("binary") converts into utf8 as if latin1 103 | 104 | 0.4.1 105 | - 20k/s 106 | - default to readImmediate:0 107 | - restify compat default to readImmediate:2, readBinary:true 108 | 109 | 0.4.0 110 | - acceptParser() restify helper 111 | - fix error classes 112 | - urldecode route params 113 | 114 | 0.3.0 115 | - deprecate after() and finally() 116 | - tune read loop 117 | 118 | 0.2.1 119 | - 21.5k/s 120 | - bugfix: write per-instance req.params not global 121 | - speedup: cache route mw stack 122 | - bodyParser(), authParser() restify helpers 123 | - fix encoders, decoders 124 | - bugfix: accepts() 125 | - bugfix: _bodyEof hang at end of call 126 | - bugfix: restify compat error handling 127 | - setNoDelay option 128 | - speedups 129 | - expose createServer 130 | - mw.closeResponse() 131 | - add 'setup' chain 132 | - fix #search query string parsing 133 | - fix 'finally' steps 134 | 135 | 0.1.0 136 | - improve restify compat (response handling) 137 | - speedup: faster run stack 138 | - fix body param decoding 139 | - fix route matching edge case 140 | - improve error handling 141 | - use() stack 142 | 143 | 0.0.1 144 | - 17k/s 145 | - basics working 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Andras Radics 2 | andras at andrasq dot com 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | restiq 2 | ====== 3 | 4 | Smaller, lighter, faster framework for REST APIs. 5 | 6 | Lean and fast for low-latency micro-services where overhead is important. 7 | Depending on the app, can serve 20k requests / second or more. 8 | 9 | There are not a lot of frills, but provides route mapping, route decoding, 10 | pre-, post- and per-route middleware stacks. Errors are caught and converted 11 | into HTTP 500 responses. Unmapped routes return 405 errors. The calls 12 | themselves can return any HTTP status code. 13 | 14 | Optionally some `restify` compatibility. I was able to swap out restify in a 15 | fairly complex app and have all its unit tests pass, and the app runs 40-50% more 16 | calls per second on restiq than on restify. 17 | 18 | var restiq = require('restiq'); 19 | var app = restiq.createServer(); 20 | 21 | app.addRoute('GET', '/', function(req, res, next) { 22 | res.end('Hello, world.'); 23 | next(); 24 | }); 25 | app.listen(1337); 26 | 27 | 28 | Objectives 29 | ---------- 30 | 31 | Why yet another framework? I wanted 32 | 33 | - to run as fast the node built-in http.createServer() (or faster; see below) 34 | - user-defined output formats (tbd) 35 | - different output formats call by call (tbd) 36 | - fewer built-ins in favor of more add-ons 37 | - to better understand the components of nodejs web service implementations, 38 | and there is no better way to learn than by doing 39 | 40 | 41 | Comparison 42 | ---------- 43 | 44 | A small echo server, parses and returns the url query parameters: 45 | 46 | - [restiq] - 20.9k/s 47 | - [http] - 17.6k/s 48 | - [express] - 7.9k/s 49 | - [restify] - 4.6k/s (8k/s using just the http methods) 50 | - [hapi] - 0.2k/s* (1.8k/s with `setNoDelay()`) 51 | (loop over the hapi sockets hashed in `reply.request.connection._connections`) 52 | 53 | \* - there is a res.write() issue with http.ServerResponse. Calls writing or 54 | piping the response run at precisely 25 requests/second per connection. 55 | It is very easily reproducible; the fix is to turn off the Nagle algorithm 56 | on the response socket with `res.socket.setNoDelay()`. 57 | 58 | 59 | Overview 60 | -------- 61 | 62 | A web service responds to requests sent to pathname-like addresses 63 | ("routes"). The server extracts the request parameters, looks up the 64 | computation associated with the route, runs it, and returns the generated 65 | response. 66 | 67 | Parameters can be embedded in the request path itself (path parameters), 68 | appended to the path in HTTP query string format (a `?` followed by 69 | '&'-separated name=value pairs, eg `?a=1&b=2`), or be in the request body in 70 | HTTP query string format or some other serialization format eg JSON or BSON. 71 | Restiq knows about path params and on-path and in-body HTTP query params. 72 | 73 | The computation is composed of a series of steps (the "middleware stack"), 74 | each step a function taking the request, the response thus far, and a callback 75 | to call to indicate that the step is finished, `(req, res, next)`. The steps 76 | are run in sequence, each called after the preceding one has finished. 77 | 78 | The steps are highly configurable. They can be run on a route-by-route basis 79 | or in common to all routes. Steps in common can be either before or after the 80 | per-route steps. In addition, steps can be configured to run after all other 81 | processing is complete even in case of errors. 82 | 83 | The restiq request and responses are just node [http.IncomingMessage] and 84 | [http.ServerResponse] objects. 85 | 86 | Restiq includes a thin compatibility layer for shimming simple 87 | [restify] applications onto restiq. 88 | 89 | 90 | Examples 91 | -------- 92 | 93 | Surprisingly, it is possible to build on top of http and achieve better 94 | throughput than a canonical http server as shown below. Because RegExps are 95 | very fast in node, extracting path params is only 5% slower. (Timed with 96 | node-v0.10.29 on an AMD 3.6 GHz 4x Phenom II.) 97 | 98 | Canonical server using [http]: 99 | 100 | var http = require('http'); 101 | var querystring = require('querystring'); 102 | var server = http.createServer(function(req, res) { 103 | req.data = ""; 104 | req.on('data', function(chunk) { req.data += chunk; }); 105 | req.on('end', function() { 106 | var url = req.url, qs = url.indexOf('?'); 107 | if (qs >= 0) req.params = querystring.parse(url.slice(qs+1)); 108 | res.writeHead(200, {'Content-Type': 'application/json'}); 109 | res.end(JSON.stringify(req.params)); 110 | }); 111 | }); 112 | server.listen(1337, '127.0.0.1'); 113 | // 17.6k/s wrk -d8s -t2 -c8 'http://localhost:1337/echo?a=1' 114 | 115 | With [restiq]: 116 | 117 | var restiq = require('restiq'); 118 | var app = restiq.createServer({readImmediate: 0}); 119 | app.addStep(restiq.mw.parseQueryParams); 120 | app.addRoute('GET', '/echo', [ 121 | function(req, res, next) { 122 | res.writeHeader(200, {'Content-Type': 'application/json'}); 123 | res.end(JSON.stringify(req.params)); 124 | next(); 125 | } 126 | ]); 127 | app.listen(1337); 128 | // 20.9k/s wrk -d8s -t2 -c8 'http://localhost:1337/echo?a=1' 129 | 130 | With [restify]: 131 | 132 | var restify = require('restify'); 133 | var app = restify.createServer(); 134 | app.use(restify.queryParser()); 135 | app.get('/echo', function(req, res, next) { 136 | res.send(200, req.params); 137 | next(); 138 | }); 139 | app.listen(1337); 140 | // 4.6k/s wrk -d8s -t2 -c8 'http://localhost:1337/echo?a=1' 141 | 142 | Change just the first two lines to run it under [restiq]: 143 | 144 | var restify = require('restiq'); 145 | var app = restify.createServer({restify: true}); 146 | // ... 147 | // 16.2k/s wrk -d8s -t2 -c8 'http://localhost:1337/echo?a=1' 148 | 149 | 150 | Methods 151 | ------- 152 | 153 | ### restiq( options ) 154 | ### restiq.createServer( options ) 155 | 156 | Create a new app. The `createServer` method returns a newly created app with 157 | no routes and no middleware steps that is not yet listening for connections. 158 | `restiq()` as a function is not a constructor but a builder, it creates a new 159 | app just like `createServer` does. (Being a builder is similar to `express`, 160 | `createServer` is similar to `http` and `restify`) 161 | 162 | The options: 163 | 164 | - `debug` - include stack traces in error responses. Be cautious about 165 | sending backtraces off-site. 166 | - `setNoDelay` - turn off Nagle write-combining on the current socket. 167 | This can greatly reduce call latency when responses use write() 168 | over an internal low-latency network. Do NOT disable Nagle for 169 | responses sent over the public internet. 170 | - `restify` - make the response have methods `res.send` for easier 171 | compatibility with restify. This eats into the throughput some, so 172 | use only as needed. 173 | - `createServer` - the function to use to create the server. It will be 174 | called with the function(req, res) that processes web requests. 175 | Exposed for testing, this defaults to `http.createServer`. 176 | - `readBinary` - when reading the request body, gather the chunks into 177 | a Buffer instead of a utf8 string. Gathering to string is faster, 178 | but Buffers are more traditional for binary data. 179 | - `readImmediate` - when reading the request body, the loop can iterate with 180 | different strategies. If set to 0 (the default), it uses `setTimeout` which 181 | supports the highest throughput under load. Set to 1 for `setImmediate` and 182 | the highest throughput with just a few active connections. Set to 2 for 183 | `on('data')`, which is in between the two -- not as fast as the others, but 184 | not as slow either. As a rule of thumb, at 8 active connections or above 185 | 0 (`setTimeout`) will offer the highest throughput. 186 | 187 | var restiq = require('restiq'); 188 | var app = restiq.createServer(options); 189 | 190 | 191 | ### app.listen( port, [hostname], [backlog], [confirmationCallback] ) 192 | 193 | Start the service. If given, `confirmationCallback` will be invoked when 194 | the service is ready to receive requests. Hostname and backlog as for 195 | `http.createServer`. 196 | 197 | ### app.setErrorHandler( onError(req, res, err, next) ) 198 | 199 | Use the provided `onError` function to handle middleware errors. The default error 200 | handler extracts an http status code from `err`, else returns a 500 Internal Server 201 | Error response. 202 | 203 | ### app.addStep( func, [where] ) 204 | 205 | Add a processing step to the middleware stack. Each step is a function 206 | `step(req, res, next)` taking request, response and a next-step callback. 207 | `func` is a step function or an array of step functions. The optional `where` 208 | specifies in which section of the middleware chain to insert the step; the 209 | default is 'use'. 210 | 211 | The middleware sections are: 212 | 213 | - `setup`, shared steps before the call is routed 214 | - `use`, partially shared steps before the route handlers are run 215 | - (the route handlers, installed with `addRoute`) 216 | - `after`, shared steps after the call successfully finished 217 | - `finally`, shared steps run in every case after all other steps have finished 218 | 219 | Middleware steps are run in the above section order, and steps within a 220 | section are run in the order added. Shared steps are run by all calls. Use 221 | steps are partially shared, and are run by only those routes that were added 222 | after the use step had already been added. I.e. if use step and routes are 223 | added interleaved, not all routes will run all use steps; all routes will run 224 | those use steps that are added before the first route is added. The route 225 | handler steps are defined per route and added with `addRoute()`. 226 | 227 | The setup steps provide an opportunity to edit the route, i.e. implement route 228 | aliasing, version mapping, etc. The use and route steps implement the 229 | call processing proper. The after steps are for shared post-call wrapup, for 230 | successful calls. The finally steps are run as the call teardown, and can do 231 | the logging, analytics reporting, etc. 232 | 233 | ### app.addRoute( method, path, [options], handlers ) 234 | 235 | Register a path along with a middleware step function (or array of functions) 236 | to handle requests for it. Returns a route object that can be used to remove 237 | and re-add the route. Requesting a path that has not been registered or 238 | calling a path with a different GET, POST, etc request method than it was 239 | registered with results in a 405 error. 240 | 241 | Paths can embed named parameters, denoted with `/:paramName`. Named 242 | parameters are extracted and stored into `req.params` (see also 243 | `restiq.mw.parseRouteParams` below). 244 | 245 | For restify compatibility, mapped routes execute those `use` steps that 246 | existed when the route was mapped. In the sequence `use`, `use`, `map(1)`, 247 | `use`, `map(2)`, calls that request route 1 will run only the first two `use` 248 | steps, but calls that request route 2 will run all three. All calls will run 249 | all `finally` steps (if any). 250 | 251 | Options: 252 | 253 | - TBD; none right now. 254 | 255 | 256 | ### app.removeRoute( route ) 257 | 258 | Remove a previously added route. The removed route can be re-added later with 259 | `addRoute(route)`. 260 | 261 | 262 | ### app.mapRoute( method, path ) 263 | 264 | For internal use, look up the route for the call. 265 | 266 | The mapped route includes the requested `path`, the matching route `name`, the 267 | `tail` of the query string with the query parameters, any named parameter 268 | `vars` included, and the list of `handlers` to run for this request. 269 | 270 | For example: 271 | 272 | app.addRoute('GET', '/:color/echo', echoColor) 273 | app.mapRoute('GET', '/green/echo?a=1&b=2') 274 | // => { 275 | // path: '/green/echo?a=1&b=2', 276 | // name: '/:color/echo', 277 | // tail: '?a=1&b=2', 278 | // vars: {color: "green"}, 279 | // handlers: [echoColor] 280 | // } 281 | 282 | Note getting the route extracts only the path params; the query string 283 | params can be gotten with `app.mw.parseQueryParams()`. 284 | 285 | 286 | restiq.mw 287 | --------- 288 | 289 | A library of pre-written middleware utility functions. Each middleware is 290 | also exposed through a configurable factory function. 291 | 292 | ### restiq.mw.parseQueryParams( req, res, next ) 293 | 294 | Merge the query string parameters into `req.params`. `buildParseQueryParams()` 295 | returns this middleware function. 296 | 297 | ### restiq.mw.parseRouteParams( req, res, next ) 298 | 299 | Merge the parameters embedded in the request path into `req.params`. This is 300 | done automatically as soon as the route is mapped, but explicit param parsing 301 | can override these values. Re-merging allows control of the param source 302 | precedence. `buildParseRouteParams()` returns this middleware function. 303 | 304 | ### restiq.mw.parseBodyParams( req, res, next ) 305 | 306 | Merge the query parameters from the body into `req.params`. Will read 307 | the body with `restiq.mw.readBody` if it has not been read already. 308 | `buildParseBodyParams(options)` returns this middleware function. 309 | 310 | Options as for `buildReadBody`. 311 | 312 | ### restiq.mw.readBody( req, res, next ) 313 | 314 | Gather up the message that was sent with the http request, and save it in 315 | `req.body`. This call is safe to call more than once, but sets body only the 316 | first time. 317 | 318 | `buildReadBody(options)` returns the readBody middleware function. 319 | 320 | Options 321 | 322 | - `maxBodySize` - The maximum request body size to allow, in bytes. 323 | Exceeding this value results in a `400 Bad Request` error response. 324 | There is no limit set by default. 325 | 326 | ### restiq.mw.discardBody( req, res, next ) 327 | 328 | Reads and discards request body to force the `end` event on the request. 329 | `buildDiscardBody()` returns this middleware function. 330 | 331 | ### restiq.mw.skipBody( req, res, next ) 332 | 333 | If the request body is guaranteed to be empty, it is faster to skip waiting 334 | for the `on('end')` event. Be careful when using this: if the request has a 335 | body it needs to be consumed. `buildSkipBody` returns this middleware function. 336 | 337 | 338 | Restify Compatibility Layer 339 | --------------------------- 340 | 341 | This is what I have so far -- 342 | 343 | 344 | ### app.pre( func ) 345 | 346 | Add shared middleware step to be called before every request, before the 347 | request is routed. Pre steps are called in the order added. 348 | 349 | 350 | ### app.use( func ) 351 | 352 | Add shared middleware step to be called before every request after the `pre()` 353 | steps have all finished. Each routed call will run only those use steps that 354 | existed at the time it was added; use steps added after a route is added will 355 | not be run by that route. Use steps are run in the order added. 356 | 357 | 358 | ### app.get( path, handler, [handler2, ...] ) 359 | 360 | Add a GET route, with handlers to run in the order listed 361 | 362 | 363 | ### app.post( path, handler, [handler2, ...] ) 364 | 365 | Add a POST route, with handlers to run in the order listed 366 | 367 | 368 | ### app.put( path, handler, [handler2, ...] ) 369 | 370 | Add a PUT route, with handlers to run in the order listed 371 | 372 | 373 | ### app.delete( path, handler, [handler2, ...] ) 374 | 375 | Add a DELETE route, with handlers to run in the order listed. 376 | This call is also available as `app.del`. 377 | 378 | 379 | ### restiq.queryParser( ) 380 | 381 | Returns a middleware `function(req, res, next)` that will extract the http query 382 | string parameters and place them in `req.params` 383 | 384 | 385 | ### restiq.authorizationParser( ) 386 | 387 | Returns a middleware `function(req, res, next)` that will decode an 388 | `Authorization: Basic` header and set the fields `req.authorization.username`, 389 | `req.authorization.basic.username` and `req.authorization.basic.password`. 390 | 391 | 392 | ### restiq.bodyParser( ) 393 | 394 | Returns a middleware `function(req, res, next)` that will decode the request 395 | body into an object, string or Buffer. The decoding is ad-hoc based on the 396 | incoming data type, and is not driven by the request headers. 397 | 398 | 399 | ### restiq.acceptParser( ) 400 | 401 | Sets the response content-encoding to the preferred (first) acceptable 402 | response type specified in the request that is supported by the server. 403 | Restiq assumes the acceptable encodings are listed in order of preference. 404 | Throws a 406 Not Acceptable error if no match is found. 405 | 406 | 407 | ### req.getId( ) 408 | 409 | returns the request id contained in the request headers. Unlike restify, 410 | restiq uses a dash `-` if can't find one, and does not make one up. 411 | 412 | 413 | ### req.version( ) 414 | 415 | Returns the options.version string that was passed to `createServer()`. 416 | 417 | 418 | ### req.header( name, [defaultValue] ) 419 | 420 | Return the named header field, or `defaultValue` if that header field was not 421 | specified in the request. 422 | 423 | 424 | ### req.path( ) 425 | 426 | Returns `req.url`. 427 | 428 | 429 | ### res.header( name, value ) 430 | 431 | Set a header value, aka `writeHeader`. 432 | 433 | 434 | ### res.get( name ) 435 | 436 | Read back a set header value. 437 | 438 | 439 | ### res.send( [statusCode], [response] ) 440 | 441 | Send a response. The default status code is 200, the default response the 442 | empty string. The call determines the content type from the response value, 443 | and emits an appropriate header as well. NOTE: restify strongly penalizes a 444 | response that does not set the Content-Type header. Time it yourself. 445 | 446 | Turns out restify responses are also extensions of `http.ServerResponse`, so 447 | all the usual write(), writeHead(), end() work as well. 448 | 449 | 450 | Tips 451 | ---- 452 | 453 | Random observations on building fast REST services 454 | 455 | - nodejs `http` has a speed-of-light of around 27k queries per second (empty 456 | request body, plaintext response) 457 | - having to assemble the body from the chunks limits http to under 24.5k/s 458 | (that's if not also parsing request params) 459 | - using req.on('data') to assemble the body drops the ceiling to under 19k/s. 460 | It is much faster to req.read() in an setTimeout loop than to wait for 461 | events. Actual times are sensitive to node version, so check. 462 | - query string params are faster to use than REST path params (because routing 463 | for static paths is a single hash lookup, vs a for loop over a list of 464 | regexp objects). Even though path params are faster to extract with a regexp 465 | than parsing the query string, it does not make up for the routing latency. 466 | - this may be obvious, but passing just path or just query params is faster than 467 | passing both 468 | - using `res.write()` to reply imposes a throttle of 25 requests per 469 | connection. Workaround is to set `res.socket.setNoDelay()` to disable the 470 | TCP/IP Nagle algorithm. Only disable for local traffic, never across the internet. 471 | 472 | 473 | Todo 474 | ---- 475 | 476 | - make sure that buffers are concatinated and returned as buffers 477 | - compat: res._body should be set to the send() object, not the stringified copy 478 | 479 | - if route not found, look in a "method-less" routes table to customize error 480 | "route not mapped" vs "POST not mapped" 481 | - make configurable what error to return on "route not mapped" (ie, restify 405 Method Not Allowed vs generic 404 Not Found) 482 | - unit tests 483 | - expose internal functions for testability 484 | - would be handy to have decodeReqBody for decoding JSON and BSON request bodies 485 | - would be handy to have encodeResBody for encoding JSON and BSON response bodies 486 | - describe the built-in restify compatibily adapter 487 | - make restiq apps emit the underlying http server events 488 | - make RestifyqRest only relay events if listened for (to maintain correct semantics) 489 | - write buildRequireParams(opts) that returns a middleware function that looks for 490 | required/optional/unknown params 491 | - double-check the restify compatibility calls, only pass the arguments 492 | that exist! else code that uses arguments.length will break 493 | - make request processing time out to close the connection (w/o response) after ? 60 sec ? 494 | - add app.set(), app.get(), app.delete() methods for key/value properties 495 | - app.use() has a two-argument form? (path, handler) ? (...express?) 496 | - missing app.head() method 497 | - key off of "Accept: text/plain" etc headers for encoding format to use 498 | - ? allow routing regexp routes ? 499 | - handle both base64 and json-array Buffer (binary) data 500 | - should support gzipped responses, 'Accept-Encoding: gzip' (chunked only!) 501 | - expose reg.log to mw functions 502 | - split rlib into misc and mw 503 | - (Q: how to pass app state in to steps? attach app to req? or ...cleaner?) 504 | - ? accept routeName handlers, to hand off to another call (... conditionally??) 505 | - compat: look for Accept-Version: header (and InvalidVersion error) 506 | - res.send() should use registered formatters (default is built-in auto-detect) 507 | - separate output formatting from Content-Type: allow for a post-formatting step 508 | to subsequently change the content type. Look for _isFormatted = true. 509 | This also allows for pluggable mw formatters, for per-call formats (eg, json 510 | for data, plaintext for metadata) 511 | - compat: re-emit all events from http.Server 512 | - compat: emit restify error events, see http://mcavage.me/node-restify/ #Server+Api 513 | - compat: expose address(), listen(), close() 514 | - compat: make parsed query params available in req.query 515 | - speed: time w/ bunyan vs w/ qlogger (close, 1820 vs 1750 4% restiq, 1177 vs 1066 8% restify) 516 | - revisit send(), support headers 517 | - save the response err to be available in finally steps 518 | - ? save the response body to be available in finally steps 519 | - alias the more common restify errors 520 | - support express app.locals and res.locals 521 | - add disable/enable/disabled methods on .restiq, for app state (express compat) 522 | - make app.* calls chainable (eg app.addRoute(), etc) 523 | - make case-insensitive routing an option (downcase path) 524 | - populate req.query et al 525 | - make readBinary a call-by-call option? eg readBodyBinary vs readBodyText 526 | - make routing a mw step, to help w/ path rewriting (to route, edit, re-route) 527 | - support limit on max request size? (error out if too big) 528 | - call versioning 529 | - time koa, meteor, (sails = express,) derby, socketstream mvc frameworks 530 | 531 | Related Work: 532 | - [express] - https://expressjs.com/ 533 | - [hapi] - https://hapijs.com/ 534 | - [http] - https://nodejs.org/api/http.html 535 | - [http.IncomingMessage] - https://www.nodejs.org/api/http.html#http_http_incomingmessage 536 | - [http.ServerResponse] - https://www.nodejs.org/api/http.html#http_class_http_serverresponse 537 | - [restify] - http://restify.com/ 538 | - [restiq] - https://www.npmjs.com/package/restiq 539 | - [fastify] - https://fastify.io 540 | 541 | [express]: https://www.npmjs.com/package/express 542 | [hapi]: https://www.npmjs.com/package/hapi 543 | [http]: https://nodejs.org/api/http.html 544 | [http.IncomingMessage]: https://www.nodejs.org/api/http.html#http_http_incomingmessage 545 | [http.ServerResponse]: https://www.nodejs.org/api/http.html#http_class_http_serverresponse 546 | [restify]: https://www.npmjs.com/package/restify 547 | [restiq]: https://www.npmjs.com/package/restiq 548 | [fastify]: https://fastify.io 549 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | //require('qtimers'); 2 | 3 | if (process.argv[2] === 'restify' || process.argv[2] === 'emulated' || process.argv[2] === 'emulate') { 4 | var Framework = (process.argv[2] === 'restify') ? require('restify') : require('../index'); 5 | var app = Framework.createServer({ 6 | name: 'test', 7 | version: '0.0.0', 8 | restify: true, 9 | //readImmediate: 0, 10 | //readBinary: false, 11 | debug: 1 12 | }); 13 | app.use(Framework.queryParser()); 14 | app.get('/echo', function(req, res, next) { 15 | res.send(req.params); 16 | next(); 17 | }); 18 | app.get('/:p1/:p2/echo', function(req, res, next) { 19 | res.send(req.params); 20 | next(); 21 | }); 22 | app.listen(1337, function(){ 23 | console.log("%s listening on 1337", process.argv[2]) 24 | }); 25 | 26 | // wrk -d20s -t2 -c8 'http://localhost:1337/echo?a=1&b=2&c=3' 27 | // => 4.3k requests / second native restify (both query and query+path params) 28 | // => 18.2k/s emulated w/ kds compatible restiq (18.6 on first 2s run, then drops) 29 | // => 14.5k/s emulated w/ 0.2.0 30 | // => 15.2k/s emulated w/ 0.4.0 31 | // => 14.2k/s emulated w 0.5.0 (but 15.4 w readBinary:false) 32 | // 16.3k/s emulated w 0.5.1 binary, read=2 33 | } 34 | else { 35 | var Restiq = require('../index'); 36 | var app = Restiq.createServer({ 37 | // 0 is faster than 2, 1 messes with gc (uses lots of memory) 38 | //readBinary: false, 39 | readImmediate: 0, 40 | // note: 1 aggravates gc, 0 is nice, 2 is ok 41 | }); 42 | app.addStep(app.mw.parseQueryParams); 43 | app.addRoute('GET', '/echo', function(req, res, next) { 44 | res.writeHead(200, {'Content-Type': 'application/json'}), 45 | res.end(JSON.stringify(req.params)), 46 | next(); 47 | }); 48 | app.addRoute('GET', '/:p1/:p2/echo', function(req, res, next) { 49 | res.writeHead(200, {'Content-Type': 'application/json'}), 50 | res.end(JSON.stringify(req.params)), 51 | next(); 52 | }); 53 | app.listen(1337, function(){ 54 | console.log("Restiq listening on 1337") 55 | }); 56 | 57 | // wrk -d20s -t2 -c8 'http://localhost:1337/echo?a=1&b=2&c=3' 58 | // => 20.0k requests / second sT, 17k/s on'data' 59 | // => 20.9k/s 0.5.1 sT, 17.9k/s on'data' 60 | } 61 | -------------------------------------------------------------------------------- /http-client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('qhttp/http-client'); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/restiq.js'); 2 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * http error builder 3 | * for each http error eg 404 Not Found creates a new error class 4 | * ErrorNotFound with err.code = 404 and default message 5 | * 6 | * Copyright (C) 2015,2017 Andras Radics 7 | * Licensed under the Apache License, Version 2.0 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var util = require('util'); 13 | var http = require('http'); 14 | 15 | 16 | for (var statusCode in http.STATUS_CODES) { 17 | var label = http.STATUS_CODES[statusCode].replace(/[^a-zA-Z0-9_]/g, ''); 18 | var name = 'Error' + label; 19 | module.exports[name] = makeError(name, statusCode, label, http.STATUS_CODES[statusCode]); 20 | module.exports[statusCode] = module.exports[name]; 21 | } 22 | 23 | // restify compat errors 24 | // module.exports['InvalidCredentialsError'] = makeError('InvalidCredentialsError', 401, 'InvalidCredentials', http.STATUS_CODES[401]); 25 | module.exports.InvalidCredentialsError = function InvalidCredentialsError( parts ) { 26 | var err = new Error(); 27 | err.statusCode = 401; 28 | err.statuscode = 'InvalidCredentials'; 29 | err.body = { 30 | code: 'InvalidCredentials', 31 | message: 'InvalidCredentials', 32 | } 33 | if (parts && parts.message !== undefined) err.message = parts.message; 34 | for (var k in parts) err.body[k] = parts[k]; 35 | return err; 36 | } 37 | 38 | function escapeQuotes( str ) { 39 | return str.replace('\\', '\\\\').replace('\'', '\\\''); 40 | } 41 | 42 | function makeError( name, code, label, message ) { 43 | // build a new Error constructor of the given name, have it inherit from Error 44 | // use eval() to give each error class a unique name (eval binds to scope) 45 | var builder = 46 | "function " + name + "(umsg) {\n" + 47 | " Error.call(this, umsg || '" + escapeQuotes(message) + "');\n" + 48 | " Error.captureStackTrace(this, this.constructor);\n" + 49 | " this.code = code;\n" + 50 | " this.statusCode = code;\n" + // restify compat 51 | " this.statuscode = '" + label + "';\n" + // restify compat 52 | " // work around the message not being set by Error.call()\n" + 53 | " this.message = umsg || '" + escapeQuotes(message) + "';\n" + 54 | "}\n" + 55 | "util.inherits(" + name + ", Error);\n" + 56 | "" + name + ";\n" + 57 | ""; 58 | var func = eval(builder); 59 | return func; 60 | } 61 | -------------------------------------------------------------------------------- /lib/qrestify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * restify compatibility functions 3 | * 4 | * Copyright (C) 2015,2017-2018 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = { 11 | emulateRestify: emulateRestify, 12 | addRestifyErrorHandler: addRestifyErrorHandler, 13 | addRestifyMethodsToReqRes: addRestifyMethodsToReqRes, 14 | }; 15 | 16 | 17 | // load and augment Restiq class 18 | var Restiq = require('./restiq.js'); 19 | 20 | /* 21 | * restify middleware insertion functions 22 | */ 23 | Restiq.prototype.pre = function restify_pre( func ) { 24 | this.addStep(func, 'setup'); 25 | }; 26 | Restiq.prototype.use = function restify_use( func ) { 27 | this.addStep(func, 'use'); 28 | }; 29 | 30 | /* 31 | * restify route creation functions 32 | */ 33 | Restiq.prototype._addRestifyRoute = function _addRestifyRoute( method, path, argv ) { 34 | // v8 will not optimize the callers of this function, they pass us arguments 35 | var i, handlers = new Array(); 36 | for (i=1; i= 0) err = null; 78 | next(err); 79 | }) 80 | } 81 | }; 82 | Restiq.authorizationParser = function restify_authorizationParser( ) { 83 | // TODO: handles Basic, but not Signature 84 | return Restiq.mw.parseAuthorization; 85 | }; 86 | Restiq.acceptParser = function restify_acceptParser( ) { 87 | // TODO: verify that this is enough 88 | return function(req, res, next) { 89 | var contentType = req.accepts(req.restiq.acceptable); 90 | if (!contentType) throw new req.restiq.errors.ErrorNotAcceptable("Server accepts: " + req.restiq.acceptable.join(",")); 91 | res.setHeader('Content-Type', contentType); 92 | next(); 93 | } 94 | }; 95 | 96 | /* 97 | * Restify defines a set of error classes as class properties 98 | */ 99 | Restiq.InvalidCredentialsError = Restiq.errors.InvalidCredentialsError; 100 | 101 | // configure req and res to behave kinda like in restify 102 | function emulateRestify( req, res, next ) { 103 | var app = req.restiq; 104 | addRestifyMethodsToReqRes(req, res); 105 | if (next) next(); 106 | } 107 | 108 | function addRestifyErrorHandler( app ) { 109 | app.setErrorHandler(function(req, res, err, next) { 110 | if (!res.headersSent) { 111 | // only restify errors have .statusCode and .body 112 | res.writeHead(err.statusCode || 500, {'Content-Type': 'application/json'}); 113 | res.end(JSON.stringify(err.body ? err.body : {code: err.code, message: err.message, stack: this._debug ? err.stack : null})); 114 | } 115 | }); 116 | } 117 | 118 | // make our http.ServerResponse behave kinda like restify.res 119 | function addRestifyMethodsToReqRes( req, res ) { 120 | res.header = function res_header( name, value ) { 121 | return value === undefined ? this.getHeader(name) : this.setHeader(name, value); 122 | }; 123 | res.send = function res_send( statusCode, body, headers) { 124 | if (body === undefined) { 125 | if (typeof statusCode === 'number') body = ""; 126 | else { body = statusCode; statusCode = 200; } 127 | } 128 | var encoding, content; 129 | 130 | // send auto-detects the encoding type from the response body sent, 131 | // one of Object, Buffer, or Error (we also accept plaintext) 132 | // TODO: match response format to req Accept: header 133 | // note: reading accept = headers['accept'] is pretty slow, 14k vs 16k/s 134 | // note: 10% faster to inline the encoding vs via encoders table 135 | // note: restify sends application/json by default, even for text 136 | if (typeof body !== 'object') encoding = 'text/plain'; 137 | else if (body) { 138 | if (body.message && body.stack && body instanceof Error) { 139 | statusCode = body.statusCode || 500; 140 | encoding = 'application/json'; 141 | // TODO: convert generic error into restify error response 142 | } 143 | else if (typeof body.length === 'number' && Buffer.isBuffer(body)) encoding = 'application/octet-stream'; 144 | else encoding = 'application/json'; 145 | } 146 | else encoding = 'application/json'; 147 | 148 | // TODO: use registered encoders for encoding 149 | // eg content = req.restiq.mw.responseEncoders[encoding](body); 150 | if (encoding === 'application/json') content = JSON.stringify(body); 151 | else if (encoding === 'application/octet-stream') content = body; 152 | else /*if (encoding === 'text/plain')*/ content = body + ""; 153 | 154 | // TODO: properly utf-8 encode the response 155 | 156 | // emulate _body, containing the un-encoded object as received 157 | res._body = body; 158 | 159 | res.statusCode = statusCode; 160 | res.setHeader('Content-Type', encoding); 161 | res.setHeader('Content-Length', content.length); 162 | 163 | // set all the specified headers, letting caller override defaults 164 | for (var k in headers) res.setHeader(k, headers[k]); 165 | 166 | if (statusCode === 204 || statusCode === 304 || req.method === 'HEAD') this.end(); 167 | else this.end(content); 168 | }; 169 | res.get = function res_get(name) { 170 | return this.getHeader(name); 171 | }; 172 | res.json = function res_json(statusCode, body) { 173 | if (body === undefined) { body = statusCode; statusCode = this.statusCode ? this.statusCode : 200; } 174 | this.send(statusCode, JSON.stringify(body), { 'Content-Type': 'application/json' }); 175 | res._body = body; 176 | }; 177 | // TODO: 178 | // charSet(type) 179 | // cache([type], [options]) 180 | // status(statusCode) 181 | 182 | req.getId = function res_getId() { 183 | return this.headers['x-request-id'] || this.headers['request-id'] || "-"; 184 | }; 185 | req.version = function req_version() { 186 | return this.restiq._opts.version; 187 | }; 188 | req.header = function req_header(name, defaultValue) { 189 | var value = this.headers[name.toLowerCase()]; 190 | return (value || value !== undefined) ? value : defaultValue; 191 | }; 192 | req.accepts = function req_accepts( types ) { 193 | // TODO: this is here only for restify acceptParser(). 194 | // TODO: either deprecate, or use in a Restiq.acceptParser (tbd) 195 | if (typeof types === 'string') types = [types]; 196 | var acceptTypes = this.headers['accept']; 197 | if (!acceptTypes || acceptTypes.indexOf('*/*') >= 0) return types[0]; 198 | 199 | // restify decorates req with a method accepts() that, when passed the app.acceptable 200 | // returns the "best" supported type for this request. 201 | // do not map(), iterate to stop early on first match 202 | acceptTypes = acceptTypes.split(';'); 203 | for (var i=0; i= 0) return types[idx]; 207 | if (type.indexOf('/') < 0) { 208 | var aliases = { 209 | 'text': 'text/plain', 210 | 'html': 'text/html', 211 | 'json': 'application/json', 212 | }; 213 | if (aliases[type]) return aliases[type]; 214 | } 215 | } 216 | // no match found, force a json response? or error out? 217 | return false; 218 | }; 219 | req.path = function req_path( ) { 220 | return this.url; 221 | }; 222 | // TODO: 223 | // is(type) 224 | // isSecure() 225 | // isChunked() 226 | // isKeepAlive() 227 | // getLogger() 228 | // time() 229 | } 230 | 231 | // accelerate access 232 | Restiq.prototype = toStruct(Restiq.prototype); 233 | Restiq = toStruct(Restiq); 234 | 235 | function toStruct( x ) { 236 | return toStruct.prototype = x; 237 | } 238 | -------------------------------------------------------------------------------- /lib/qroute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Quick REST route mapping and lookup 3 | * 4 | * Copyright (C) 2015,2017 Andras Radics 5 | * Licensed under the Apache License, Version 2.0 6 | * 7 | * 2015-01-08 - AR. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | module.exports = QRoute; 13 | 14 | function QRoute( ) { 15 | this._literals = {}; 16 | this._patterns = new Array(); 17 | } 18 | 19 | /** 20 | * register the handlers associated with the route routeName 21 | */ 22 | QRoute.prototype.addRoute = function addRoute( routeName, handlers ) { 23 | if (typeof routeName !== 'string') { 24 | // reinsert removed route 25 | var route = routeName.type === 'mappedRoute' && routeName._route || routeName; 26 | if (route.type === 'lit') this._literals[info.name] = route; 27 | else this._patterns.push(route); 28 | } 29 | else if (routeName.indexOf("/:") < 0) { 30 | var info = {type: 'lit', name: routeName, method: null, handlers: handlers, steps: 0, stack: null}; 31 | this._literals[routeName] = info; 32 | return info; 33 | } 34 | else { 35 | var match = this._buildCapturingRegex(routeName); 36 | var info = {type: 'patt', name: routeName, method: null, handlers: handlers, patt: match.patt, names: match.names, steps: 0, stack: null}; 37 | this._patterns.push(info); 38 | return info; 39 | } 40 | }; 41 | 42 | /** 43 | * remove a route 44 | */ 45 | QRoute.prototype.removeRoute = function removeRoute( info ) { 46 | if (info.type === 'mappedRoute') info = info._route; 47 | // TODO: maybe also match by routeName, so can remove /path/name like add /path/name 48 | 49 | if (info.type === 'lit') { 50 | // TODO: maybe support multiple routes for the same path? 51 | delete this._literals[info.name]; 52 | } 53 | else { 54 | var idx = this._patterns.indexOf(info); 55 | if (idx >= 0) this._patterns.splice(idx, 1); 56 | } 57 | } 58 | 59 | /** 60 | * clear all middleware stacks cached in the routes 61 | */ 62 | QRoute.prototype.clearMwStacks = function clearMwStacks( ) { 63 | var i; 64 | for (i in this._literals) this._literals[i].stack = null; 65 | for (i=0; i= 0) { tail = querypath.slice(qmark+1); querypath = querypath.slice(0, qmark); } 76 | if ((route = this._literals[querypath])) { 77 | return { 78 | type: 'mappedRoute', 79 | path: querypath, 80 | name: route.name, 81 | tail: tail || "", 82 | vars: {}, 83 | _route: route, 84 | }; 85 | } 86 | var patterns = this._patterns; 87 | for (var i=0; i 0) pattern += this._regexEscape(routeName.slice(0, match.index)); 114 | pattern += '\\/([^/]*)'; 115 | names.push(match[0].slice(2)); 116 | routeName = routeName.slice(match.index + match[0].length); 117 | } 118 | pattern += this._regexEscape(routeName); 119 | // the route matches if the query string ends here or continues only past / or ? 120 | pattern += "([/?].*)?$"; 121 | return {patt: new RegExp(pattern), names: names}; 122 | }; 123 | 124 | /** 125 | * backslash-escape the chars that have special meaning in regex strings 126 | */ 127 | QRoute.prototype._regexEscape = function _regexEscape( str ) { 128 | // For PCRE or POSIX, the regex metacharacters are: 129 | // . [ ( - terms 130 | // * + ? { - repetition specifiers 131 | // | - alternation 132 | // \ - escape char 133 | // ^ $ - anchors 134 | // ) - close paren (else invalid node regex) 135 | // Matching close chars ] } are not special without the open char. 136 | // / is not special in a regex, it matches a literal /. 137 | // : and = are not special outside of [] ranges or (?) conditionals. 138 | // ) has to be escaped always, else results in "invalid regex" 139 | return str.replace(/([.[(*+?{|\\^$=)])/g, '\\$1'); 140 | }; 141 | 142 | QRoute.prototype = toStruct(QRoute.prototype); 143 | 144 | function toStruct( x ) { 145 | return toStruct.prototype = x; 146 | } 147 | 148 | 149 | // quickest: 150 | /** 151 | 152 | var timeit = require('qtimeit'); 153 | 154 | var f = new QRoute(); 155 | f.addRoute('GET::/foo/bar', 1); 156 | f.addRoute('POST::/:kid/b]ar/:collection/:op', 2); 157 | console.log(f.mapRoute('POST::/kid/b]ar/collection/op/zed?a=1')); 158 | 159 | //timeit(100000, function(){ f.mapRoute('POST::/foo/bar') }); 160 | 161 | timeit(100000, function(){ f.mapRoute('GET::/foo/bar') }); 162 | // 3m/s (2.6m/s node-v0.11.13) 163 | // (but only 1.4m/s if mapped w/ regex... => regex param capturing is free) 164 | // 19.5m/s without the routeName string concat !! 165 | 166 | timeit(100000, function(){ f.mapRoute('POST::/kid/bar/collection/op') }); 167 | // 1.47m/s (single regex) (1.53m/s node-v0.11.13) 168 | // ...ie, 50 routes is at most 27k requests mapped / sec (so mapped routes *halve* the service rate) 169 | // 4m/s without the routeName string concat !! 170 | // to avoid concat: have caller create an array of QRoute mappers, one per GET,POST etc method 171 | 172 | //console.log(f); 173 | 174 | /**/ 175 | -------------------------------------------------------------------------------- /lib/restiq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * rest framework for micro-services 3 | * ...speed, speed, speed 4 | * 5 | * Copyright (C) 2015,2017 Andras Radics 6 | * Licensed under the Apache License, Version 2.0 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var http = require('http'); 12 | var util = require('util'); 13 | var EventEmitter = require('events').EventEmitter; 14 | 15 | // faster to predefine the fields that restiq appends to req 16 | http.IncomingMessage.prototype.headers = null; 17 | http.IncomingMessage.prototype.restiq = null; 18 | http.IncomingMessage.prototype.query = null; 19 | http.IncomingMessage.prototype.params = null; 20 | http.IncomingMessage.prototype.body = ""; 21 | http.IncomingMessage.prototype._route = null; 22 | http.IncomingMessage.prototype._bodyEof = undefined; 23 | // restify adds this method, and restify users expect it (eg unit tests) 24 | http.IncomingMessage.prototype.header = function req_header(name) { 25 | return this.headers[name.toLowerCase()]; 26 | }; 27 | 28 | // also predefine restify compatibility methods added to req and res 29 | http.IncomingMessage.prototype.method = null; 30 | http.IncomingMessage.prototype.accepts = undefined; 31 | http.IncomingMessage.prototype.getId = null; 32 | http.IncomingMessage.prototype.version = null; 33 | //http.IncomingMessage.prototype.headers = {}; 34 | //http.IncomingMessage.prototype.statusCode = 200; 35 | http.IncomingMessage.prototype.authorization = null; 36 | http.IncomingMessage.prototype.username = null; 37 | // restify adds this method, and restify users expect it (eg unit tests) 38 | http.ServerResponse.prototype.header = function res_header(name, value) { 39 | if (value) this.setHeader(name, value); 40 | else return this.getHeader(name); 41 | } 42 | http.ServerResponse.prototype.send = null; 43 | http.ServerResponse.prototype.get = null; 44 | http.ServerResponse.prototype.json = null; 45 | http.ServerResponse.prototype._body = null; 46 | 47 | // accelerate access 48 | http.IncomingMessage.prototype = toStruct(http.IncomingMessage.prototype); 49 | http.ServerResponse.prototype = toStruct(http.ServerResponse.prototype); 50 | 51 | var aflow = require('aflow'); 52 | var QRoute = require('./qroute'); 53 | module.exports = Restiq; 54 | module.exports.mw = require('./rlib'); 55 | module.exports.errors = require('./errors'); 56 | 57 | function Restiq( opts ) { 58 | if (this === global || !this) return Restiq.createServer(opts); 59 | 60 | this._opts = opts || {}; 61 | this._emulateRestify = this._opts.restify; 62 | this._setNoDelay = this._opts.setNoDelay; 63 | this._debug = this._opts.debug; 64 | 65 | // middleware stacks 66 | this._setup = new Array(); // pre-routing mw 67 | this._before = new Array(); // pre-call mw 68 | this._routes = { 69 | GET: new QRoute(), // GET routed mw 70 | POST: new QRoute(), // POST mw 71 | Other: new QRoute(), // all other routed mw 72 | }; 73 | this._after = new Array(); // post-call mw 74 | this._finally = new Array(); // teardown mw 75 | 76 | this._server = null; 77 | this._errorHandler = null; 78 | 79 | if (this._emulateRestify) { 80 | if (this._opts.readImmediate === undefined) this._opts.readImmediate = 2; 81 | if (this._opts.readBinary === undefined) this._opts.readBinary = true; 82 | var restify = require('./qrestify'); 83 | restify.addRestifyErrorHandler(this); 84 | this._setup.push(restify.emulateRestify); 85 | } 86 | 87 | // disable listen on new Restiq() to allow using Restiq() as an app builder 88 | this.listen = function() { throw new Error("new Restiq() cannot listen, use Restiq.createServer()"); } 89 | } 90 | util.inherits(Restiq, EventEmitter); 91 | 92 | // bring these closer, for faster access 93 | Restiq.prototype.mw = module.exports.mw; 94 | Restiq.prototype.errors = module.exports.errors; 95 | 96 | // restify compat: the list of recognized mime types 97 | // TODO: populate this from the mime types of the configured decoders 98 | Restiq.prototype.acceptable = [ 99 | 'application/json', // json object 100 | 'text/plain', // urlencoded query string 101 | 'application/octet-stream', // Buffer object 102 | 'application/javascript', // json object ... but as Buffer? 103 | 'application/x-www-form-urlencoded', // urlencoded query string 104 | ]; 105 | 106 | Restiq.prototype.setErrorHandler = function setErrorHandler( handler ) { 107 | // handler(req, res, err, next) 108 | this._errorHandler = handler; 109 | } 110 | 111 | Restiq.prototype.setOutputHandler = function setOutputHandler( handler ) { 112 | // handler( ??? ) 113 | this._outputHandler = handler; 114 | }; 115 | 116 | Restiq.prototype.setTimeout = function setTimeout( timeout, listener ) { 117 | this._server.setTimeout(timeout, listener); 118 | }; 119 | 120 | Restiq.createServer = function createServer( opts ) { 121 | var app = new Restiq(opts); 122 | var createServer = http.createServer; 123 | if (opts) { 124 | if (opts.createServer) createServer = opts.createServer; 125 | } 126 | 127 | // undo the listen override 128 | delete app.listen; 129 | 130 | app._server = createServer( function(req, res, whenDone) { 131 | var self = app; 132 | // assign instance vars, else param parsing stores by reference into the parent! 133 | req.params = {}; 134 | 135 | // faster to setNoDelay here than to on-the-fly disable it on write() 136 | if (self._setNoDelay) res.socket.setNoDelay(); 137 | 138 | // run the setup stack before anything else, even routing 139 | req.restiq = app; 140 | setImmediate(function() { 141 | runMiddlewareStack(self, self._setup, req, res, function(err) { 142 | // error before routing even: clean up and quit 143 | if (err) return finishMiddlewareStack(self, req, res, err, whenDone); 144 | 145 | // route the call 146 | var route = self.mapRoute(req.method, req.url); 147 | // TODO: unmapped route should be 404 Not Found, but might be a breaking change 148 | if (!route) return finishMiddlewareStack(self, req, res, new self.errors.ErrorMethodNotAllowed("route not mapped"), whenDone); 149 | req._route = route; // save for parseRouteParams 150 | if (route._route.type === 'patt') self.mw.parseRouteParams(req, res); 151 | 152 | // build the middleware stack for this call 153 | var middlewareStack; 154 | if (route._route.stack) { 155 | middlewareStack = route._route.stack; 156 | } 157 | else { 158 | // restify compat: only run those mw steps that existed when the route was added 159 | self._before.limit = route._route.steps; 160 | 161 | middlewareStack = concatArrays( 162 | self._before, 163 | route._route.handlers, 164 | self._after 165 | // self._finally run unconditionally 166 | ); 167 | route._route.stack = middlewareStack; 168 | } 169 | 170 | // process the request, return the response 171 | runMiddlewareStack(self, middlewareStack, req, res, function(err) { 172 | // _finally steps are run by finishMiddlewareStack 173 | // if mocking the server, will be provided with a callback 174 | finishMiddlewareStack(self, req, res, err, whenDone); 175 | }); 176 | }); 177 | }); 178 | }); 179 | 180 | return app; 181 | } 182 | 183 | // start the service, invoke the callback once started 184 | // This is *not* used to listen to each request arrive 185 | Restiq.prototype.listen = function listen( ) { 186 | var i, args = new Array(); 187 | for (i=0; i= 0) { } else { throw err; } } 200 | }; 201 | 202 | function concatArrays( /* VARARGS */ ) { 203 | var i, j, dst = new Array(), nargs = arguments.length; 204 | for (i=0; i= 0 ? arr.limit : arr.length); 206 | for (j=0; j= len); 221 | }); 222 | }, 223 | function whenDone(err) { 224 | // caller must finish the call 225 | if (err === 'halt mw') err = null; 226 | next(err); 227 | } 228 | ); 229 | } 230 | 231 | function finishMiddlewareStack( app, req, res, err, whenDone ) { 232 | // TODO: what route to emit? 233 | // TODO: can end up in here before routing, when no route will be set! 234 | var restifyRoute = req._route ? req._route.name : req.url; 235 | 236 | if (err) app._endWithError(req, res, err); 237 | 238 | // the _finally steps are executed after every call, whether error or not 239 | runMiddlewareStack(app, app._finally, req, res, function(err2) { 240 | 241 | // fully consume request body, for connection reuse 242 | if (!req._bodyEof) app.mw.discardBody(req, res); 243 | 244 | if (err || err2) { 245 | // TODO: mimic the restify route a bit better, this is just a placeholder 246 | if (app._emulateRestify) try { app.emit('uncaughtException', req, res, restifyRoute, err) } catch (e) { } 247 | return app._endWithError(req, res, err || err2); 248 | if (err && err2) console.log("restiq: mw error in 'finally' stack:", err2.stack) 249 | } 250 | // restify suppresses errors thrown in `after` 251 | if (app._emulateRestify) try { app.emit('after', req, res, restifyRoute, err); } catch(e) { } 252 | 253 | // NOTE: do NOT force an end(), a poorly written call can have 254 | // unfinished queued continuations that may send a response later. 255 | // Yes, that will mess with call timing / metrics (hooked to 'after'). 256 | // Maybe could set a timeout to force an end after a minute. 257 | // if (!res.headersSent) res.end(); 258 | 259 | if (whenDone) whenDone(); 260 | }) 261 | } 262 | 263 | Restiq.prototype._endWithError = function _endWithError( req, res, err ) { 264 | var self = this; 265 | 266 | // be sure to consume all input, even in case of error. 267 | // Either that, or close the connection and force the client to reconnect. 268 | if (!req._bodyEof) this.mw.discardBody(req, res); 269 | 270 | // if the response has already been sent, cannot instead send an error 271 | if (res.headersSent) return; 272 | 273 | // generate an http error response 274 | if (this._errorHandler) { 275 | // use the provided errorHandler to generate an http error response 276 | this._errorHandler(req, res, err, function(err2) { 277 | if (err2) console.log("restiq: mw error in service errorHandler:", err2.stack); 278 | }); 279 | } 280 | else { 281 | // use built-in logic to extract an http error response from err 282 | var code = 500, message = "middleware error"; 283 | if (err.statusCode >= 100 && err.statusCode <= 999) { code = err.statusCode, message = err.message; } 284 | else if (err.code >= 100 && err.code <= 999) { code = err.code, message = err.message; } 285 | 286 | // TODO: have the commit() hook format the response, and 287 | // TODO: do not set statusCode or emit response here 288 | 289 | // if no response sent yet, send the error 290 | // TODO: make top-level error response configurable (pluggable handler) 291 | res.writeHead(code, {'Content-Type': 'text/plain'}); 292 | if (this._debug > 0) message += "; " + err.stack; 293 | res.end(message); 294 | } 295 | return err; 296 | }; 297 | 298 | // ---------------------------------------------------------------- 299 | 300 | Restiq.prototype.addStep = function addStep( func, where ) { 301 | if (typeof func === 'string') { var t = where; where = func; func = t; } 302 | where = where || 'use'; 303 | if (Array.isArray(func)) { 304 | for (var i=0; i= 0) { 69 | var hmark = req.url.indexOf('#'); 70 | if (hmark < 0) hmark = req.url.length; 71 | err = _tryDecodeQuery(req.url.slice(qmark+1, hmark), req.query = {}); 72 | for (var i in req.query) req.params[i] = req.query[i]; 73 | } 74 | if (next) next(err); 75 | }; 76 | }; 77 | 78 | // decode body, or return err 79 | function _tryDecodeBody( req, type ) { 80 | // if could decode then change req.body to the decoded object, else leave as-is 81 | try { var body = decodeBody(type, req.body); if (body !== undefined) req.body = body; } 82 | catch (e) { return new Restiq.errors.ErrorBadRequest("error decoding body params"); } 83 | } 84 | 85 | // parse message body and store resulting object in req.body 86 | function buildParseBody( options ) { 87 | var readBody = buildReadBody(options); 88 | return function parseBody( req, res, next ) { 89 | function isBase64( str, limit ) { 90 | var charp = str.charCodeAt ? str.charCodeAt : function(i) { return str[i] }; 91 | var len = Math.min(limit, str.length); 92 | for (var i=0; i= 0x30 && c <= 0x39 || // [0-9] 97 | c >= 0x41 && c <= 0x5a || // [A-Z] 98 | c >= 0x61 && c <= 0x7a || // [a-z] 99 | c === 0x2b || // [+] 100 | c === 0x2f || // [/] 101 | c === 0x3d || // [=] 102 | c === 0x0d || // [\r] 103 | c === 0x0a)) // [\n] 104 | return false; 105 | } 106 | return true; 107 | } 108 | 109 | readBody(req, res, function(err) { 110 | if (err) return next ? next(err) : null; 111 | if (!err) { 112 | // FIXME: we key off Content-Type to determine how to decode, 113 | // but this should be exposed and configurable via the app 114 | var type = req.headers['content-type']; 115 | // FIXME: tentative: content-type auto-detection 116 | if (!type) { 117 | // auto-detect type based on content if no content-type 118 | if (req.body[0] === '{' || req.body[0] === 0x7b) type = 'application/json'; 119 | else if (req.body[0] === '[' || req.body[0] === 0x5b) type = 'application/octet-stream'; 120 | else if (isBase64(req.body, 2000)) type = 'base64'; 121 | else type = 'application/octet-stream'; 122 | } 123 | if (type !== 'text/plain') { 124 | err = _tryDecodeBody(req, type); 125 | } 126 | } 127 | if (next) next(err); 128 | }); 129 | } 130 | }; 131 | 132 | // Consumes request data immediately so the end event is fired. 133 | // Data gathered is ignored. 134 | function buildDiscardBody() { 135 | return function discardBody( req, res, next ) { 136 | req.on('data', function() {}); 137 | req.on('error', function(err) { 138 | req._bodyEof = true; 139 | var msg = "Error attempting to dispose the request body" + (req.restiq._opts.debug ? (": " + err.stack) : ""); 140 | if (next) next(new Restiq.errors.ErrorInternalServerError(msg)); 141 | }); 142 | req.on('end', function() { 143 | req._bodyEof = true; 144 | if (next) next(); 145 | }); 146 | }; 147 | } 148 | 149 | function buildParseBodyParams( options ) { 150 | var readBody = buildReadBody(options); 151 | return function parseBodyParams( req, res, next ) { 152 | if (!req._bodyEof) { 153 | // if body has not been read yet, read it first 154 | readBody(req, res, function(err) { 155 | if (err) return next ? next(err) : null; 156 | parseBodyParams(req, res, next); 157 | }); 158 | } 159 | else { 160 | // FIXME: we key off Content-Type to determine how to decode, 161 | // but this should be exposed and configurable via the app 162 | var err, type = req.headers && req.headers['content-type'] || 'text/plain'; 163 | try { decodeBody(type, req.body, req.params); } 164 | catch (e) { err = new Restiq.errors.ErrorBadRequest("error decoding body params"); } 165 | if (next) next(err); 166 | } 167 | }; 168 | }; 169 | 170 | function buildParseRouteParams() { 171 | return function parseRouteParams( req, res, next ) { 172 | // module params were extracted with the route match regex, transcribe them 173 | var i, params = req.params, vars = req._route.vars; 174 | for (i in vars) params[i] = http_parse_query.urldecode(vars[i]); 175 | if (next) next(); 176 | }; 177 | }; 178 | 179 | function buildSkipBody() { 180 | return function skipBody( req, res, next ) { 181 | // CAUTION: only use if can guarantee that there is no body 182 | // (eg, when used in strictly controlled environment) 183 | req.body = ""; 184 | req._bodyEof = true; 185 | if (next) next(); 186 | } 187 | }; 188 | 189 | 190 | function buildReadBody(options) { 191 | options = options || {}; 192 | 193 | return function readBody( req, res, next ) { 194 | // body is read only once ever, and _bodyEof is the mutex 195 | if (req._bodyEof !== undefined) return next ? next() : null; 196 | req._bodyEof = false; 197 | 198 | var bytesReceived = 0; 199 | var data = ""; 200 | var chunks = new Array(); 201 | var readBinary = req.restiq && req.restiq._opts.readBinary; 202 | var readImmediate = req.restiq && req.restiq._opts.readImmediate; 203 | var _returned = false; 204 | 205 | // have the system deal with not splitting multi-byte chars 206 | if (!readBinary) req.setEncoding('utf8'); 207 | 208 | // default to on('data'), not the fastest but more versatile 209 | // 2 is 15% less max throughput than 0 and 10% slower than 1, 210 | // but scales down better than 0 and doesnt affect gc like 1 211 | if (readImmediate === undefined) readImmediate = 2; 212 | 213 | function combineChunks( chunks ) { 214 | if (chunks.length > 1) return Buffer.concat(chunks); 215 | if (chunks.length > 0) return chunks[0]; 216 | return new Buffer(""); 217 | } 218 | function returnOnce( err ) { 219 | if (!_returned) { 220 | _returned = true; 221 | if (next) next(err); 222 | } 223 | } 224 | function gatherChunk(chunk) { 225 | // NOTE: it is faster to concat strings than to push the buffers 226 | // NOTE: toString('binary') is not binary, it converts from latin-1 227 | bytesReceived += chunk.length; 228 | 229 | if (options.maxBodySize && bytesReceived > options.maxBodySize) { 230 | var readErr = new Restiq.errors.ErrorBadRequest('Error reading body, max request body size exceeded.'); 231 | returnOnce(readErr); 232 | } 233 | 234 | if (readBinary) chunks.push(chunk); 235 | else data += chunk; 236 | } 237 | function readloop() { 238 | if (!req._bodyEof) { 239 | var chunk = req.read(); 240 | 241 | if (chunk) { 242 | gatherChunk(chunk); 243 | } 244 | 245 | if (!readImmediate) { 246 | // 35% higher peak throughput with setTimeout (2800/s vs 2050) 247 | // 3x higher throughput per connection with setImmediate (1700/s vs 550) 248 | // +10% peak throughput when using qtimers (3000/s, 2300/s) 249 | setTimeout(readloop, 1); 250 | } 251 | else { 252 | // Note: setImmediate internally can chew up lots of memory and perform poorly 253 | // in wrk -d8s -t2 -c8 throughput tests. But then it performs better in kds 254 | // and in restify emulation mode; else it`s about even. 255 | // needs qtimers setImmediate else can trample the gc system 256 | // TODO: retime under newer node 257 | setImmediate(readloop); 258 | } 259 | } 260 | } 261 | // consume data to trigger the 'end' event 262 | // read() is 40% quicker than on('data') (v0.10.29) 263 | // TODO: time out after some amount of inactivity! 264 | if (readImmediate == 2) req.on('data', gatherChunk); 265 | else readloop(); 266 | 267 | req.on('error', function(err) { 268 | req._bodyEof = true; 269 | var msg = "error reading request body" + ((req.restiq && req.restiq._opts.debug) ? (": " + err.stack) : ""); 270 | returnOnce(new Restiq.errors.ErrorInternalServerError(msg)); 271 | }); 272 | 273 | req.on('end', function() { 274 | req._bodyEof = true; 275 | req.body = data || combineChunks(chunks); 276 | returnOnce(); 277 | }); 278 | }; 279 | }; 280 | 281 | // restify compatible Basic auth header parsing 282 | // the auth info is stored in req.authorization.basic and req.authorization.username 283 | function buildParseAuthorization() { 284 | return function parseAuthorization( req, res, next ) { 285 | var auth = req.headers['authorization']; 286 | if (!auth) return next(); 287 | var parts = auth.split(' '); 288 | if (parts[0] === 'Basic' || parts.toLowerCase[0] === 'basic') { 289 | var nameval = new Buffer(parts[1], 'base64'); 290 | for (var i=0; i0.0.0" 17 | }, 18 | "scripts": { 19 | "test": "qnit test", 20 | "coverage": "nyc --include lib --include index.js --reporter lcov --reporter text npm test", 21 | "clean": "rm -rf .nyc_output coverage" 22 | }, 23 | "keywords": [ 24 | "Andras", 25 | "quick", 26 | "fast", 27 | "rest", 28 | "framework", 29 | "restify", 30 | "express" 31 | ], 32 | "dependencies": { 33 | "aflow": "0.10.1", 34 | "qhttp": "0.6.1" 35 | }, 36 | "devDependencies": { 37 | "qnit": "0.18.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/quicktest1.js: -------------------------------------------------------------------------------- 1 | if (process.argv[1].indexOf('nodeunit') >= 0 || process.argv[1].indexOf('qnit') >= 0) return; 2 | 3 | // quicktest: 4 | 5 | var cluster = require('cluster'); 6 | var http = require('http'); 7 | 8 | if (0 && cluster.isMaster) { 9 | cluster.fork(); 10 | cluster.fork(); 11 | cluster.fork(); 12 | } 13 | else { 14 | 15 | //require('qtimers'); 16 | //setImmediate.maxTickDepth = 1; 17 | // process.versions.node = '0.10.29' 18 | 19 | var server1 = http.createServer( function(req, res) { 20 | var data = ""; 21 | var eof = false; 22 | 23 | // consume data (and discard) to trigger the 'end' event 24 | function readloop() { 25 | if (!eof) { 26 | // read() is 40% quicker than on('data') (v0.10.29) 27 | var chunk = req.read(); 28 | if (chunk) data += chunk; 29 | //setImmediate(readloop); // 16k/s 10.29 and 11.13 30 | setTimeout(readloop, 1); // 22.5k/s 10.29, 6k/s 11.13 31 | // 22.5k/s v0.10.29 using setTimeout loop, 16k/s on('data') 32 | // 15.5k/s v0.11.13 using setImmediate loop, 15.5k/s on('data') 33 | } 34 | } 35 | //data = req.read(); 36 | readloop(); 37 | 38 | //req.on('data', function(){}); 39 | req.on('end', function(){ 40 | eof = true; 41 | req.body = data; 42 | res.writeHead(200, {'Content-Type': 'text/plain'}); 43 | res.end('Hello World\n'); 44 | //res.end(json.encode({a:1,b:2,c:3,d:4,e:5})); 45 | }); 46 | 47 | // 27k/s to send reply and ignore data 48 | // 25.5k/s send JSON 5 params 49 | // 26.3k/s send json-simple 5 params 50 | // 19k/s if processing on the 'end' (40% faster w/o data) 51 | // 24.5k/s if triggering 'end' with read() (instead of with on('data')) 52 | // 22.5k/s if triggering 'end' with setTimeout read loop (23.5k/s w/ qtimers) 53 | // 16k/s w/ setImmediate loop (24k/s setImmediate loop with qtimers !! 50% faster !! :-) (17k/s qtimers maxTickDepth=1) 54 | // 24k/s if calling readloop but not waiting for it to finish 55 | // BUT: above 22.5k/s is 5k/s with node-v0.11.13 !! (15.5k/s w/ on('data') and w/ setImmediate loop) 56 | // BUT: qtimers setImmediate is slower! (15k/s) than native, and qtimers setTimeout is 6k/s 57 | }); 58 | server1.listen(1337, '127.0.0.1'); 59 | console.log('Server running at http://127.0.0.1:1337/'); 60 | // 27k/s 61 | 62 | var mustWaitForBody = { 63 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 64 | 'OPTIONS': 1, 65 | 'GET': 1, 66 | 'HEAD': 0, 67 | 'PUT': 1, 68 | 'POST': 1, 69 | 'DELETE': 1, 70 | 'TRACE': 1, 71 | 'CONNECT': 0, 72 | }; 73 | var server1b = http.createServer( function(req, res) { 74 | var data = ""; 75 | var type = req.headers['content-type'] || 'text/plain'; 76 | var url = req.url; 77 | 78 | // handle connection: 79 | // - if route expects streaming input, do not assemble chunks 80 | // - else if no body expected, do not assemble chunks (ignore body) 81 | // - else assemble chunks and set req.body 82 | // - middleware step decodes query params, body params as configured 83 | // - before steps: in common to all routes 84 | // - after steps: in common to all routes 85 | // - per-route steps: per route 86 | // - NO BUILT-IN STEPS! the default is a blank slate (no parsing, no response) 87 | // - override res to capture headers, emit them all together 88 | // with eg send(statusCode, response) and sendHeader(name, value) 89 | 90 | if (mustWaitForBody[req.method]) { 91 | req.on('data', function(chunk) { 92 | data += chunk; 93 | }); 94 | req.on('error', function(err) { 95 | res.writeHead(500); 96 | res.end("request error"); 97 | }); 98 | req.on('end', function() { 99 | data = '{"a":1,"b":2,"c":3,"d":4,"e":5}'; 100 | req.body = data; 101 | req.params = {}; 102 | decodeBodyParams(req, res, function(){}); 103 | 104 | // FIXME: merge query params and body params 105 | res.writeHead(200, {'Content-Type': 'text/plain'}); 106 | //res.end('Hello World\n'); 107 | res.end(JSON.stringify(req.params)); 108 | }); 109 | } 110 | else { 111 | req.params = {}; 112 | req.body = ""; // suppress body 113 | res.writeHead(200, {'Content-Type': 'text/plain'}); 114 | res.end('Hello World\n'); 115 | } 116 | 117 | //res.end(json.encode({a:1,b:2,c:3,d:4,e:5})); 118 | // 27k/s 119 | // 25.5k/s JSON parsing 5 params 120 | // 26.3k/s json-simple 5 params 121 | // 22.5k/s if decoding 5 query string args 122 | // 19.4k/s if assembling the post data from chunks 123 | // 16.4k/s if parsing 5 json body params from post request (25 vs 16: EventEmitter overhead, waiting for 'end') 124 | // 16.0k/s if parsing both query params and post data params (5 + 5 fields) (18k/s w/o query params) 125 | }); 126 | //server1b.listen(1337, '127.0.0.1'); 127 | //console.log('Server running at http://127.0.0.1:1337/'); 128 | // 27k/s 129 | 130 | var server2 = http.createServer(function(req, res) { 131 | // 132 | res.statusCode = 200; // avoid! 4% slower 133 | res.setHeader('Content-Type', 'text/plain'); // avoid! 17% slower 134 | //res.sendDate = true; 135 | res.write("Hello, world."); 136 | res.end(); 137 | //res.end(JSON.stringify({done:1})); 138 | //res.write(JSON.stringify({done:1})); // AVOID! slows to 25/sec per connection 139 | //res.writeHeader(200, { 'Content-Type': 'application/json', }); 140 | //res.end(JSON.stringify({done:1})); // 25.5k/s 141 | //res.end(json.encode({done:1})); // 26.4k/s 142 | //res.end("Hello, world."); // 27k/s 143 | //res.writeHeader(200, { 'Content-Type': 'application/json', }); 144 | //res.end('{"done":1}'); 145 | // 23k/s for header + end 146 | // 28k/s w/o headers (w. or w/o date formatting) 147 | // 27k/s w/ writeHeader 148 | // 23k/s w/ setHeader (instead of writeHeader) (w. or w/o statusCode) 149 | // 25/s per thread write + end !? (flushes once every 40ms??) (peak ~5000/sec, limited by procs/fd's) 150 | // 27k/s if end() writes all the data too (do not call write!) 151 | // 23k/s if setHeader() used + end() -- use writeHeader() instead, *much* faster !? 152 | // 25k/s if statusCode set *and* writeHeader() used 153 | // 26k/s if JSON.stringify, 27k/s if string contstant, 154 | }); 155 | //server2.listen(1337, undefined, 2047); 156 | //console.log("listening on 1337..."); 157 | // 158 | // wth?? capped at 200 connections / sec ?! 159 | 160 | 161 | // server1.listen(1337); 162 | // console.log("Server running at http://localhost:1337"); 163 | } 164 | 165 | 166 | var paramDecoders = { 167 | 'text/plain': function(s){ return http_parse_query(s); }, 168 | 'x-www-form-urlencoded': function(s){ return http_parse_query(s); }, 169 | 'application/json': function(s){ return JSON.parse(s); }, 170 | // make this be installed externally by the app config! 171 | // 'application/bson': function(s){ return BSONPure.decode(s); } 172 | }; 173 | 174 | function decodeParams( type, str, params ) { 175 | var decode, ps; 176 | ps = ((decode = paramDecoders[type])) ? decode(str) : {}; 177 | // FIXME: make http_parse_query accept an optional object to populate with fields, 178 | // since iterating an object is much slower than passing it to the function 179 | for (var i in ps) params[i] = ps[i]; 180 | } 181 | 182 | function decodeQueryParams( req, res, next ) { 183 | var qmark, queryParams = {}; 184 | if ((qmark = req.url.indexOf('?') >= 0)) { 185 | decodeParams('x-www-form-urlencoded', req.url.slice(qmark+1), req.params); 186 | } 187 | next(); 188 | } 189 | 190 | function decodeBodyParams( req, res, next ) { 191 | var type = req.headers['content-type'] || 'text/plain'; 192 | if (req.body) { 193 | decodeParams(type, req.body, req.params); 194 | } 195 | next(); 196 | } 197 | -------------------------------------------------------------------------------- /test/sample.js: -------------------------------------------------------------------------------- 1 | if (process.argv[1].indexOf('nodeunit') >= 0 || process.argv[1].indexOf('qnit') >= 0) return; 2 | 3 | require('qtimers') 4 | 5 | // sample Restiq app 6 | 7 | var cluster = require('cluster'); 8 | var Restiq = require('../index'); 9 | 10 | echoStack = [ 11 | //Restiq.mw.parseQueryParams, 12 | //Restiq.mw.readBody, 13 | //Restiq.mw.parseBodyParams, 14 | //Restiq.mw.skipBody, 15 | function(req, res, next) { 16 | res.writeHeader(200, {'Content-Type': 'application/json'}); 17 | res.end(JSON.stringify(req.params)); 18 | next(); 19 | } 20 | ]; 21 | 22 | if (0 && cluster.isMaster) { 23 | cluster.fork(); 24 | cluster.fork(); 25 | cluster.fork(); 26 | // cluster.fork(); 27 | } 28 | else { 29 | var app = Restiq.createServer({ 30 | //debug: 1, 31 | //setNoDelay: true, 32 | //readImmediate: 2, 33 | //readBinary: true, 34 | }); 35 | app.addStep(app.mw.parseQueryParams); 36 | //app.pre(Restiq.mw.skipBody); 37 | //app.pre(app.mw.readBody); 38 | //app.pre(app.mw.parseBodyParams); 39 | app.addRoute('GET', '/echo', echoStack); 40 | app.addRoute('POST', '/echo', echoStack); 41 | // 18.8k/s static routes w/ 5 url query params 42 | // wrk -d20s -t2 -c8 43 | // => 0.2.0: 19.1k/s (but 0.2.0 was buggy) 44 | // => 0.3.0: 19.3k/s utf8, 18.2k/s binary 5 query params 45 | // => 0.4.0: 19.6k/s utf8 46 | // 20.5k/s static routes w/ 5 query params, skipped body (...why not closer to 27k/s?) 47 | /** 48 | app.addRoute('GET', '/:parm1/:parm2/echo1', echoStack); 49 | app.addRoute('GET', '/:parm1/:parm2/echo2', echoStack); 50 | app.addRoute('GET', '/:parm1/:parm2/echo3', echoStack); 51 | app.addRoute('GET', '/:parm1/:parm2/echo4', echoStack); 52 | app.addRoute('GET', '/:parm1/:parm2/echo5', echoStack); 53 | app.addRoute('GET', '/:parm1/:parm2/echo6', echoStack); 54 | app.addRoute('GET', '/:parm1/:parm2/echo7', echoStack); 55 | app.addRoute('GET', '/:parm1/:parm2/echo8', echoStack); 56 | app.addRoute('GET', '/:parm1/:parm2/echo9', echoStack); 57 | app.addRoute('GET', '/:parm1/:parm2/echo10', echoStack); 58 | app.addRoute('GET', '/:parm1/:parm2/echo11', echoStack); 59 | app.addRoute('GET', '/:parm1/:parm2/echo12', echoStack); 60 | app.addRoute('GET', '/:parm1/:parm2/echo13', echoStack); 61 | app.addRoute('GET', '/:parm1/:parm2/echo14', echoStack); 62 | app.addRoute('GET', '/:parm1/:parm2/echo15', echoStack); 63 | app.addRoute('GET', '/:parm1/:parm2/echo16', echoStack); 64 | app.addRoute('GET', '/:parm1/:parm2/echo17', echoStack); 65 | app.addRoute('GET', '/:parm1/:parm2/echo18', echoStack); 66 | app.addRoute('GET', '/:parm1/:parm2/echo19', echoStack); 67 | app.addRoute('GET', '/:parm1/:parm2/echo20', echoStack); 68 | **/ 69 | app.addRoute('GET', '/:parm1/:parm2/echo', echoStack); 70 | // app.addRoute('POST', '/:parm1/:parm2/echo', echoStack); 71 | // 20.3k/s parametric routes (2 parametric, no echo) 72 | // => 0.5.0 21.7k/s 2 path params, no echo 73 | // 17.7k/s 2 parametric + 5 echo 74 | //app.addRoute('GET', '/echo2/:parm1/:parm2/:parm3/:parm4/:parm5', echoStack); 75 | app.addRoute('GET', '/:parm1/:parm2/:parm3/:parm4/:parm5/echo', echoStack); 76 | app.addRoute('POST', '/:parm1/:parm2/:parm3/:parm4/:parm5/echo', echoStack); 77 | // 19.3k/s 5 path params w/o any query params 78 | // 16.7k/s 5 path params + 5 query params 79 | // => 0.3.0: 20.0k/s 5 path params w/o query params (strings) 80 | // => 0.5.0: 17.3k/s 5 path params w 5 query params 81 | 82 | app.listen(1337); 83 | console.log("Server listening on 1337"); 84 | } 85 | -------------------------------------------------------------------------------- /test/test-errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var errors = require('../lib/errors'); 9 | 10 | module.exports = { 11 | 'should export 401 error': function(t) { 12 | t.ok(errors.ErrorUnauthorized); 13 | t.equal(errors[401], errors.ErrorUnauthorized); 14 | var err = new errors[401](); 15 | t.equal(err.statusCode, 401); 16 | t.equal(err.message, 'Unauthorized'); 17 | t.done(); 18 | }, 19 | 20 | 'should export InvalidCredentialsError': function(t) { 21 | t.ok(errors.InvalidCredentialsError); 22 | var err = new errors.InvalidCredentialsError(); 23 | t.equal(err.statusCode, 401); 24 | t.deepEqual(err.body, { code: 'InvalidCredentials', message: 'InvalidCredentials' }); 25 | t.done(); 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /test/test-package.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | module.exports = { 9 | 'should parse package.json': function(t) { 10 | var json = require('../package.json'); 11 | t.equal(json.name, "restiq"); 12 | t.done(); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/test-qroute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var QRoute = require('../lib/qroute'); 9 | 10 | module.exports = { 11 | setUp: function(done) { 12 | this.cut = new QRoute(); 13 | done(); 14 | }, 15 | 16 | 'should add and get route': function(t) { 17 | var route = this.cut.addRoute('/echo', ['mw-stack']); 18 | var route2 = this.cut.mapRoute("/echo?a=1"); 19 | t.ok(route2._route === route, "mapped _route differs from the added route"); 20 | t.done(); 21 | }, 22 | 23 | 'mapped route should have path, name, tail': function(t) { 24 | var route = this.cut.addRoute('/echo', ['mw-stack']); 25 | var route2 = this.cut.mapRoute("/echo?a=1"); 26 | t.equal(route2.path, "/echo"); 27 | t.equal(route2.name, '/echo'); 28 | t.equal(route2.tail, "a=1"); 29 | t.done(); 30 | }, 31 | 32 | 'route should capture path parameters': function(t) { 33 | this.cut.addRoute('/:entity/get/:field', ['mw-stack']); 34 | var route = this.cut.mapRoute('/database1/get/table2?a=1'); 35 | t.deepEqual(route.vars, { entity: 'database1', field: 'table2' }); 36 | t.done(); 37 | }, 38 | 39 | 'removeRoute': { 40 | 'should remove route': function(t) { 41 | var route = this.cut.addRoute('/echo', ['mw-stack']); 42 | t.equal(this.cut.mapRoute('/echo')._route, route); 43 | this.cut.removeRoute(route); 44 | t.equal(this.cut.mapRoute('/echo'), null); 45 | t.done(); 46 | }, 47 | 48 | 'should remove mapped route': function(t) { 49 | this.cut.addRoute('/echo', ['mw-stack']); 50 | var mappedRoute = this.cut.mapRoute('/echo'); 51 | t.equal(mappedRoute.type, 'mappedRoute'); 52 | this.cut.removeRoute(mappedRoute); 53 | t.equal(this.cut.mapRoute('/echo'), null); 54 | t.done(); 55 | } 56 | 57 | }, 58 | 59 | // TODO: write tests 60 | }; 61 | -------------------------------------------------------------------------------- /test/test-restify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017-2018 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var assert = require('assert'); 9 | var http = require('http'); 10 | var Restiq = require('../index'); 11 | var qrestify = require('../lib/qrestify'); 12 | 13 | module.exports = { 14 | setUp: function(done) { 15 | this.app = new Restiq({restify: true}); 16 | this.getMockReq = function getMockReq(url) { 17 | var req = new http.IncomingMessage(url); 18 | req.restiq = { _opts: {} }; 19 | return req; 20 | }; 21 | this.getMockRes = function getMockRes(req) { 22 | var res = new http.ServerResponse(req); 23 | return res; 24 | }; 25 | done(); 26 | }, 27 | 28 | 'should create a restify mimic app': function(t) { 29 | var app = new Restiq({restify: true}); 30 | t.done(); 31 | }, 32 | 33 | 'should set app._emulateRestify': function(t) { 34 | var app1 = new Restiq({}); 35 | var app2 = new Restiq({ restify: true }); 36 | t.ok(!app1._emulateRestify); 37 | t.equal(app2._emulateRestify, true); 38 | t.done(); 39 | }, 40 | 41 | 'should expose restify route creation calls': function(t) { 42 | var methods = ['get', 'put', 'post', 'del']; 43 | for (var i in methods) t.ok(this.app[methods[i]]); 44 | t.done(); 45 | }, 46 | 47 | 'should expose restify middleware insert calls': function(t) { 48 | var methods = ['pre', 'use']; 49 | for (var i in methods) t.ok(this.app[methods[i]]); 50 | t.done(); 51 | }, 52 | 53 | 'class should expose restify middleware library calls': function(t) { 54 | var methods = ['queryParser', 'bodyParser', 'authorizationParser']; 55 | for (var i in methods) t.ok(Restiq[methods[i]]); 56 | t.done(); 57 | }, 58 | 59 | 'should have restify-compat mw and route methods': function(t) { 60 | var i, expect = ['pre', 'use', 'get', 'put', 'post', 'del']; 61 | for (i in expect) { 62 | t.ok(typeof this.app[expect[i]] === 'function'); 63 | } 64 | t.done(); 65 | }, 66 | 67 | 'should have restify-compat methods': function(t) { 68 | t.equal(typeof Restiq.bodyParser, 'function'); 69 | t.equal(typeof Restiq.queryParser, 'function'); 70 | t.done(); 71 | }, 72 | 73 | 'should decorate restiq app with routing methods': { 74 | 'pre, use should invoke addStep': function(t) { 75 | var app = Restiq({ restify: true }); 76 | 77 | var spy = t.spy(app, 'addStep'); 78 | app.pre(function(req, res, next) { }); 79 | app.use(function(req, res, next) { }); 80 | t.equal(spy.callCount, 2); 81 | 82 | t.done(); 83 | }, 84 | 85 | 'http methods should invoke _addRestifyRoute': function(t) { 86 | var app = Restiq({ restify: true }); 87 | 88 | var spy = t.spy(app, '_addRestifyRoute'); 89 | app.get('/test', function(req, res, next) { }); 90 | app.put('/test', function(req, res, next) { }); 91 | app.post('/test', function(req, res, next) { }); 92 | app.del('/test', function(req, res, next) { }); 93 | app.head('/test', function(req, res, next) { }); 94 | app.opts('/test', function(req, res, next) { }); 95 | app.patch('/test', function(req, res, next) { }); 96 | t.equal(spy.callCount, 7); 97 | 98 | t.done(); 99 | }, 100 | }, 101 | 102 | 'should decorate res': { 103 | setUp: function(done) { 104 | this.req = this.getMockReq('/test'); 105 | this.res = this.getMockRes(this.req); 106 | qrestify.addRestifyMethodsToReqRes(this.req, this.res); 107 | done(); 108 | }, 109 | 110 | 'with send': { 111 | 'should set res._body': function(t) { 112 | var req = this.req, res = this.res; 113 | res.send(201, { x: 202 }, { 'Content-Type': 'text/plain' }); 114 | assert.equal(res.statusCode, 201); 115 | assert.deepEqual(res._body, { x: 202 }); 116 | assert.deepEqual(res.header('Content-Type'), 'text/plain'); 117 | t.done(); 118 | }, 119 | }, 120 | 121 | 'with get': { 122 | 'should return header': function(t) { 123 | var req = this.req, res = this.res; 124 | res.setHeader('abc', 123); 125 | t.equal(res.get('abc'), 123); 126 | t.done(); 127 | }, 128 | }, 129 | 130 | 'with json': { 131 | 'should set _body and content-type': function(t) { 132 | var req = this.req, res = this.res; 133 | res.setHeader('Content-Type', 123); 134 | res.json({ x: 234 }); 135 | assert.equal(res.getHeader('Content-Type'), 'application/json'); 136 | assert.deepEqual(res._body, { x: 234 }); 137 | t.done(); 138 | }, 139 | }, 140 | }, 141 | 142 | 'should decorate Restiq with middleware builders': { 143 | 'should build queryParser, bodyParser, authrorizationParser and acceptParser': function(t) { 144 | t.equal(typeof Restiq.queryParser(), 'function'); 145 | t.equal(typeof Restiq.bodyParser(), 'function'); 146 | t.equal(typeof Restiq.authorizationParser(), 'function'); 147 | t.equal(typeof Restiq.acceptParser(), 'function'); 148 | t.done(); 149 | }, 150 | }, 151 | }; 152 | -------------------------------------------------------------------------------- /test/test-restiq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var http = require('http'); 9 | var url = require('url'); 10 | var Restiq = require('../index'); 11 | var qmock = require('qnit').qmock; 12 | 13 | var HttpClient = require('../http-client'); 14 | 15 | module.exports = { 16 | 'Restiq class': { 17 | 'should have createServer method': function(t) { 18 | t.equal(typeof Restiq.createServer, 'function'); 19 | t.done(); 20 | }, 21 | 22 | 'should expose mw': function(t) { 23 | t.ok(Restiq.mw); 24 | t.ok(Restiq.mw.parseQueryParams); 25 | t.ok(Restiq.mw.parseBodyParams); 26 | t.ok(Restiq.mw.readBody); 27 | t.ok(Restiq.mw.buildParseQueryParams()); 28 | t.ok(Restiq.mw.buildParseBodyParams()); 29 | t.ok(Restiq.mw.buildReadBody()); 30 | t.done(); 31 | }, 32 | 33 | 'should expose errors': function(t) { 34 | t.ok(Restiq.errors); 35 | t.ok(Restiq.errors['405']); 36 | var e = new Restiq.errors[405]("err msg"); 37 | t.ok(e.code); 38 | t.equal(e.message, 'err msg'); 39 | t.ok(e instanceof Error); 40 | t.ok(e.stack); 41 | t.done(); 42 | }, 43 | 44 | 'error should have default message': function(t) { 45 | var e = new Restiq.errors[404](); 46 | t.equal(e.message, "Not Found"); 47 | t.done(); 48 | }, 49 | 50 | 'error should use user message': function(t) { 51 | var e= new Restiq.errors[404]('user message'); 52 | t.equal(e.message, 'user message'); 53 | t.done(); 54 | }, 55 | 56 | 'should create app': function(t) { 57 | var app = Restiq.createServer(); 58 | t.ok(app instanceof Restiq); 59 | t.done(); 60 | }, 61 | 62 | 'should create app by function': function(t) { 63 | var app = Restiq(); 64 | t.ok(app instanceof Restiq); 65 | t.ok(! app.hasOwnProperty('listen')); 66 | t.done(); 67 | }, 68 | 69 | 'should not create app by new': function(t) { 70 | var app = new Restiq(); 71 | t.expect(3); 72 | t.ok(app instanceof Restiq); 73 | t.ok(app.hasOwnProperty('listen')); 74 | try { app.listen(); t.ok(false); } 75 | catch (err) { t.ok(true); } 76 | t.done(); 77 | }, 78 | }, 79 | 80 | 'restiq mw': { 81 | 'should parse query params': function(t) { 82 | var req = {url: "/echo?a=1&b=2&c=3++4", params: {}}; 83 | t.expect(3); 84 | Restiq.mw.parseQueryParams(req, {}, function(err) { 85 | t.ok(!err); 86 | t.equal(req.params.b, 2); 87 | t.equal(req.params.c, '3 4'); 88 | t.done(); 89 | }); 90 | }, 91 | 92 | 'should parse body params (hierarchical and urldecoded)': function(t) { 93 | var req = {url: "/echo?a=1&b=2", body: "c[cc]=3&d%25=%25&e=3++4", _bodyEof: 1, params: {}}; 94 | t.expect(4); 95 | Restiq.mw.parseBodyParams(req, {}, function(err) { 96 | t.ok(!err); 97 | t.equal(req.params.c.cc, 3); 98 | t.equal(req.params['d%'], '%'); 99 | t.equal(req.params.e, '3 4'); 100 | t.done(); 101 | }); 102 | }, 103 | 104 | 'should closeResponse with encodeResponseBody': function(t) { 105 | var res = qmock.getMock({}, ['writeHead', 'end']); 106 | res.body = {}; 107 | res.expects(qmock.any()).method('getHeader').will(qmock.returnValue(undefined)); 108 | res.expects(qmock.once()).method('end').with("{}"); 109 | t.expect(1); 110 | Restiq.mw.closeResponse({}, res, function(){ 111 | t.ok(!res.check()); 112 | t.done(); 113 | }); 114 | }, 115 | 116 | 'should dispose of body': function(t) { 117 | var req = new http.IncomingMessage(); 118 | req.push("Not going to use this"); 119 | req.push(null); 120 | // Simulating setEncoding since that is not available on the mock. 121 | t.expect(1); 122 | Restiq.mw.discardBody(req, {}, function(err) { 123 | t.ok(!err); 124 | t.done(); 125 | }); 126 | }, 127 | 128 | 'should sucessfully read body as a string': function(t) { 129 | var req = new http.IncomingMessage(); 130 | req.restiq = { 131 | _opts: {} 132 | }; 133 | req.push("Just some data bro"); 134 | req.push(null); 135 | t.expect(1); 136 | Restiq.mw.readBody(req, {}, function(err) { 137 | t.ok(!err); 138 | t.done(); 139 | }); 140 | }, 141 | 142 | 'should fail to parse body due to maxBodySize being exceeded': function(t) { 143 | var req = new http.IncomingMessage(); 144 | req.restiq = { 145 | _opts: { 146 | readBinary: true 147 | } 148 | }; 149 | req.push("Just some data bro"); 150 | req.push(null); 151 | t.expect(2); 152 | Restiq.mw.buildReadBody({maxBodySize: 1})(req, {}, function(err) { 153 | t.ok(err); 154 | t.equal(err.message, 'Error reading body, max request body size exceeded.'); 155 | t.done(); 156 | }); 157 | } 158 | }, 159 | 160 | 'restiq app setup': { 161 | setUp: function(done) { 162 | this.app = Restiq.createServer(); 163 | done(); 164 | }, 165 | 166 | 'should start on listen, end on close': function(t) { 167 | var app = this.app; 168 | t.expect(2); 169 | var ok = this.app.listen(21337, function(err) { 170 | t.ifError(err); 171 | app.close(function() { 172 | t.ok(1); 173 | t.done(); 174 | }); 175 | }); 176 | }, 177 | 178 | 'should reject unmapped routes': function(t) { 179 | var app = this.app; 180 | t.expect(2); 181 | var ok = this.app.listen(21337, function(err) { 182 | t.ifError(err); 183 | app.close(function() { 184 | t.ok(1); 185 | t.done(); 186 | }); 187 | }); 188 | }, 189 | 190 | 'should have the expected mw and route methods': function(t) { 191 | var i, expect = ['addStep', 'addRoute', 'removeRoute', 'mapRoute']; 192 | for (i in expect) { 193 | t.ok(typeof this.app[expect[i]] === 'function'); 194 | } 195 | t.done(); 196 | }, 197 | 198 | 'should add and map route': function(t) { 199 | var i, methods = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'custom']; 200 | var j, routes = ['/echo', '/:x/echo']; 201 | for (i in methods) for (j in routes) { 202 | this.app.addRoute(methods[i], routes[j], [ function(req, res, next){ next() } ]); 203 | } 204 | for (i in methods) for (j in routes) { 205 | var route = this.app.mapRoute(methods[i], routes[j] + '?a=1&b=2'); 206 | t.ok(route); 207 | t.equal(route.name, routes[j]); 208 | } 209 | t.done(); 210 | }, 211 | 212 | 'should add route with options': function(t) { 213 | var route = this.app.addRoute('GET', '/echo', {opt1: 1, opt2: 2}, ['mw-stack']); 214 | t.equal(route.handlers[0], 'mw-stack'); 215 | t.done(); 216 | }, 217 | 218 | 'rest routes should extract path params': function(t) { 219 | this.app.addRoute('GET', '/:x/:y/echo', function(){}); 220 | var route = this.app.mapRoute('GET', '/1/2/echo'); 221 | t.equal(route.vars.x, 1); 222 | t.equal(route.vars.y, 2); 223 | t.done(); 224 | }, 225 | }, 226 | 227 | 'restiq app middleware': { 228 | setUp: function(done) { 229 | var self = this; 230 | this.app = Restiq.createServer(); 231 | this.app.addStep(Restiq.mw.closeResponse, 'finally'); 232 | this.httpClient = new HttpClient(); 233 | done(); 234 | }, 235 | 236 | 'should addStep': function(t) { 237 | function f1(){} 238 | function f2(){} 239 | function f3(){} 240 | function f4(){} 241 | 242 | // add middleware steps, function first 243 | this.app.addStep(f1); 244 | t.equal(this.app._before[0], f1); 245 | this.app.addStep(f2, 'setup'); 246 | t.equal(this.app._setup[0], f2); 247 | this.app.addStep(f3, 'after'); 248 | t.equal(this.app._after[0], f3); 249 | this.app.addStep(f4, 'finally'); 250 | t.equal(this.app._finally.pop(), f4); 251 | 252 | // add middleware steps, function last 253 | this.app.addStep('use', f1); 254 | t.equal(this.app._before[1], f1); 255 | this.app.addStep('setup', f2); 256 | t.equal(this.app._setup[1], f2); 257 | this.app.addStep('after', f3); 258 | t.equal(this.app._after[1], f3); 259 | this.app.addStep('finally', f4); 260 | t.equal(this.app._finally.pop(), f4); 261 | 262 | // add array of functions 263 | this.app.addStep('use', [f1, f2, f3]); 264 | t.equal(this.app._before.pop(), f3); 265 | 266 | // throws on invalid function 267 | try { this.app.addStep(123); t.fail() } 268 | catch (err) { t.contains(err.message, "must be a function") } 269 | try { this.app.addStep(f1, "noplace"); t.fail() } 270 | catch (err) { t.contains(err.message, "unknown") } 271 | 272 | t.done(); 273 | }, 274 | 275 | 'should run pre steps': function(t) { 276 | var app = this.app; 277 | var httpClient = this.httpClient; 278 | t.expect(4); 279 | app.addStep(function(req, res, next){ t.ok(1); next(); }, 'setup'); 280 | app.addRoute('GET', '/echo', [function(req, res, next) { res.end("done"); next() }]); 281 | app.listen(21337, function(err){ 282 | t.ifError(err); 283 | httpClient.call('GET', 'http://127.0.0.1:21337/echo', function(err, res) { 284 | t.ifError(err); 285 | t.equal(res.statusCode, 200); 286 | app.close(function(){ 287 | t.done(); 288 | }); 289 | }); 290 | }); 291 | }, 292 | 293 | 'should run steps in order': function(t) { 294 | // TODO: make createServer mockable 295 | //var req = qmock.getMock({}, []); 296 | //var res = qmock.getMock({}, ['end', 'writeHead', 'setHeader', 'getHeader']); 297 | //var run, app = Restiq.createServer({createServer: function(onConnect){ run = onConnect; }}); 298 | var app = this.app; 299 | var httpClient = this.httpClient; 300 | var order = []; 301 | t.expect(3); 302 | app.addStep(function(req, res, next){ order.push('finally1'); next(); }, 'finally'); 303 | app.addStep(function(req, res, next){ order.push('after1'); next(); }, 'after'); 304 | app.addStep(function(req, res, next){ order.push('use1'); next(); }, 'use'); 305 | app.addStep(function(req, res, next){ order.push('setup1'); next(); }, 'setup'); 306 | app.addStep(function(req, res, next){ order.push('finally2'); next(); }, 'finally'); 307 | app.addStep(function(req, res, next){ order.push('after2'); next(); }, 'after'); 308 | app.addStep(function(req, res, next){ order.push('use2'); next(); }, 'use'); 309 | app.addStep(function(req, res, next){ order.push('setup2'); next(); }, 'setup'); 310 | app.addRoute('GET', '/echo', [ function(q,s,n){ order.push('app1'); n() }, function(q,s,n){ order.push('app2'); n() } ]); 311 | app.listen(21337, function(err) { 312 | //run(req, res, function(err) { 313 | t.ifError(err); 314 | httpClient.call('GET', 'http://127.0.0.1:21337/echo', function(err, res) { 315 | t.ifError(err); 316 | t.deepEqual(order, ['setup1', 'setup2', 317 | 'use1', 'use2', 'app1', 'app2', 318 | 'after1', 'after2', 'finally1', 'finally2']); 319 | app.close(); 320 | t.done(); 321 | }); 322 | //}); 323 | }); 324 | }, 325 | 326 | 'should run "finally" for unmapped routes': function(t) { 327 | var self = this; 328 | var called = false; 329 | self.app.addStep('finally', function(req, res, next) { called = true; next() }); 330 | self.app.listen(21337, function(err) { 331 | t.ifError(err); 332 | self.httpClient.call('GET', 'http://localhost:21337/nonesuch', function(err, res) { 333 | t.ifError(err); 334 | t.equal(called, true); 335 | // TODO: should be a 404, not 405 336 | t.equal(res.statusCode, 405); 337 | self.app.close(); 338 | t.done(); 339 | }) 340 | }) 341 | }, 342 | 343 | 'should emit "after" when mw stack is done': function(t) { 344 | var app = this.app; 345 | app._emulateRestify = true; 346 | app.listen(21337); 347 | 348 | var stepCount = 0; 349 | app.addStep('setup', function(req, res, next) { stepCount += 1; next() }); 350 | app.addStep('use', function(req, res, next) { stepCount += 1; next() }); 351 | app.addRoute('GET', '/ping', function(req, res, next) { 352 | stepCount += 1; 353 | res.end('OK'); 354 | next(); 355 | }) 356 | 357 | var afterCalled = false; 358 | app.on('after', function(req, res, route, next) { 359 | afterCalled = stepCount; 360 | next(); 361 | }) 362 | var req = http.request("http://localhost:21337/ping", function(res) { 363 | t.equal(afterCalled, 3); 364 | app.close(); 365 | t.done(); 366 | }) 367 | req.end(""); 368 | }, 369 | 370 | 'should emit "uncaughtException" on error': function(t) { 371 | var app = this.app; 372 | app._emulateRestify = true; 373 | 374 | var calledError, testError = new Error("test error"); 375 | app.on('uncaughtException', function(req, res, route, err) { 376 | calledError = err; 377 | }) 378 | app.addStep(function(req, res, next) { 379 | throw testError; 380 | }) 381 | app.addRoute('GET', '/ping', function(req, res, next) { 382 | res.end('OK'); 383 | next(); 384 | }) 385 | app.listen(21337); 386 | 387 | this.httpClient.call('GET', 'http://localhost:21337/ping', function(err, res) { 388 | t.ifError(err); 389 | t.equal(res.statusCode, 500); 390 | t.contains(res.body, 'middleware'); 391 | t.equal(calledError, testError); 392 | app.close(); 393 | t.done(); 394 | }) 395 | }, 396 | 397 | 'should stop middleware on false': function(t) { 398 | var app = this.app; 399 | var step1called = false, step2called = false; 400 | app.addStep(function(req, res, next) { step1called = true; next(false) }); 401 | app.addStep(function(req, res, next) { step2called = true; next() }); 402 | app.addRoute('GET', '/echo', function(req, res, next) { res.end("OK") }); 403 | var self = this; 404 | app.listen(21337, function() { 405 | self.httpClient.call('GET', 'http://localhost:21337/echo', function(err, res) { 406 | t.ifError(err); 407 | t.equal(res.statusCode, 200); 408 | t.strictEqual(step1called, true); 409 | t.strictEqual(step2called, false); 410 | app.close(); 411 | t.done(); 412 | }) 413 | }) 414 | }, 415 | 416 | 'should stop middleware but run finally in case of error': function(t) { 417 | var app = this.app; 418 | var step1called = false, step2called = false, finallyCalled = false; 419 | app.addStep(function(req, res, next) { step1called = true; next(new Error("test error")) }); 420 | app.addStep(function(req, res, next) { step2called = true; next() }); 421 | app.addStep('finally', function(req, res, next) { finallyCalled = true; next() }); 422 | app.addRoute('GET', '/echo', function(req, res, next) { res.end("OK") }); 423 | var self = this; 424 | app.listen(21337, function() { 425 | self.httpClient.call('GET', 'http://localhost:21337/echo', function(err, res) { 426 | t.ifError(err); 427 | t.equal(res.statusCode, 500); 428 | t.strictEqual(step1called, true); 429 | t.strictEqual(step2called, false); 430 | t.strictEqual(finallyCalled, true); 431 | app.close(); 432 | t.done(); 433 | }) 434 | }) 435 | }, 436 | 437 | 'should stop middleware on uncaught exception': function(t) { 438 | var app = this.app; 439 | var step1called = false, step2called = false, finally1called = false, finally2called = false; 440 | app.addStep('finally', function(req, res, next) { finally1called = true; next() }); 441 | app.addStep(function(req, res, next) { step1called = true; throw new Error("test error") }); 442 | app.addStep(function(req, res, next) { step2called = true; next() }); 443 | app.addStep('finally', function(req, res, next) { finally2called = true; next() }); 444 | app.addRoute('GET', '/echo', function(req, res, next) { res.end("OK") }); 445 | var self = this; 446 | app.listen(21337, function() { 447 | self.httpClient.call('GET', 'http://localhost:21337/echo', function(err, res) { 448 | t.ifError(err); 449 | t.equal(res.statusCode, 500); 450 | t.strictEqual(step1called, true); 451 | t.strictEqual(step2called, false); 452 | t.strictEqual(finally1called, true); 453 | t.strictEqual(finally2called, true); 454 | app.close(); 455 | t.done(); 456 | }) 457 | }) 458 | }, 459 | }, 460 | }; 461 | -------------------------------------------------------------------------------- /test/test-rlib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015,2017 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict' 7 | 8 | if (process.argv[1].indexOf('nodeunit') >= 0 || process.argv[1].indexOf('qnit') >= 0) return; 9 | 10 | var http = require('http'); 11 | var qmock = require('qnit').qmock; 12 | 13 | var rlib = require('../lib/rlib'); 14 | 15 | module.exports = { 16 | setUp: function(done) { 17 | this.getMockReq = function getMockReq(url) { 18 | var req = new http.IncomingMessage(url); 19 | req.restiq = { _opts: {} }; 20 | return req; 21 | }; 22 | this.getMockRes = function getMockRes(req) { 23 | var res = new http.ServerResponse(req); 24 | return res; 25 | }; 26 | this.mockReq = this.getMockReq("http://localhost:80"); 27 | this.mockRes = this.getMockRes(this.mockReq); 28 | done(); 29 | }, 30 | 31 | tearDown: function(done) { 32 | qmock.unmockHttp(); 33 | done(); 34 | }, 35 | 36 | 'skipBody': { 37 | 'should clear body and set _bodyEof': function(t) { 38 | var req = this.mockReq, res = this.mockRes; 39 | rlib.skipBody(res, res); 40 | t.equal(res.body, ""); 41 | t.equal(res._bodyEof, true); 42 | t.done(); 43 | }, 44 | }, 45 | 46 | 'parseAuthorization': { 47 | 'should parse Authorization header': function(t) { 48 | this.mockReq.headers['authorization'] = "Basic " + new Buffer("user123:pass456").toString('base64'); 49 | rlib.parseAuthorization(this.mockReq, this.mockRes); 50 | t.equal(this.mockReq.username, "user123"); 51 | t.deepEqual(this.mockReq.authorization, { basic: { username: "user123", password: "pass456" } }); 52 | t.done(); 53 | } 54 | }, 55 | 56 | 'readBody': { 57 | setUp: function(done) { 58 | this.testReadBody = function testReadBody( chunks, cb ) { 59 | var req = this.mockReq, res = this.mockRes; 60 | for (var i=0; i 0); 167 | t.done(); 168 | }) 169 | this.mockReq.emit('error', new Error("deliberate error")); 170 | }, 171 | 172 | 'should fail if larger than maxBodySize': function(t) { 173 | var readBody = rlib.buildReadBody({ maxBodySize: 5 }); 174 | readBody(this.mockReq, this.mockRes, function(err) { 175 | t.ok(err); 176 | t.ok(err.message.indexOf(" max ") > 0); 177 | t.done(); 178 | }) 179 | this.mockReq.emit('data', "testmessage"); 180 | t.done(); 181 | }, 182 | }, 183 | }, 184 | } 185 | --------------------------------------------------------------------------------