Anda di halaman 1dari 36

mcavage.me http://mcavage.

me/node-restify/#installation
API Guide | restify
About restify
restify is a node.js module built specifically to enable you to build correct REST web services. It intentionally
borrows heavily from express as that is more or less the de facto API for writing web applications on top of node.js.
Why use restify and not express?
I get asked this more than anything else, so I'll just get it out of the way up front.
Express' use case is targeted at browser applications and contains a lot of functionality, such as templating and
rendering, to support that. Restify does not.
Restify exists to let you build "strict" API services that are maintanable and observable. Restify comes with
automatic DTrace support for all your handlers, if you're running on a platform that supports DTrace.
In short, I wrote restify as I needed a framework that gave me absolute control over interactions with HTTP and full
observability into the latency and characteristics of my applications. If you don't need that, or don't care about
those aspect(s), then it's probably not for you.
About this guide
This guide provides comprehensive documentation on writing a REST api (server) with restify, writing clients that
easily consume REST APIs, and on the DTrace integration present in restify.
Note this documentation refers to the 2.x version(s) of restify; these versions are not backwards-compatible with
the 0.x and 1.x versions.
If you're migrating from an earlier version of restify, see 1.4 to 2.0 Migration Tips
Conventions
Any content formatted like this:
curl
localhost:8080
is a command-line example that you can run from a shell. All other examples and information is formatted like this:
GET /foo
HTTP/1.1
Installation
npm install
restify
Server API
The most barebones echo server:
var restify = require('restify');
function respond(req, res, next) {
res.send('hello ' + req.params.name);
next();
}
var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);
server.listen(8080, function() {
console.log('%s listening at %s', server.name,
server.url);
});
Try hitting that with the following curl commands to get a feel for what restify is going to turn that into:
curl -is http://localhost:8080/hello/mark -H 'accept: text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Date: Mon, 31 Dec 2012 01:32:44 GMT
Connection: keep-alive
hello mark
$ curl -is http://localhost:8080/hello/mark
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:33:33 GMT
Connection: keep-alive
"hello mark"
$ curl -is http://localhost:8080/hello/mark -X HEAD -H 'connection:
close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:42:07 GMT
Connection: close
Note that by default, curl uses
Connection: keep-
alive . In order to make the HEAD method return right away,
you'll need to pass
Connection:
close .
Since curl is often used with REST APIs, restify provides a plugin to work around this idiosyncrasy in curl. The
plugin checks whether the user agent is curl. If it is, it sets the Connection header to "close" and removes the
"Content-Length" header.
server.pre(restify.pre.userAgentConnection());
See the pre method for more information.
Creating a Server
Creating a server is straightforward, as you simply invoke the createServer API, which takes an options object
with the options listed below (and listen() takes the same arguments as node's http.Server.listen):
var restify = require('restify');
var server =
restify.createServer({
certificate: ...,
key: ...,
name: 'MyApp',
});
server.listen(8080);
Option Type Description
certificate String If you want to create an HTTPS server, pass in the PEM-encoded certificate and
key
key String If you want to create an HTTPS server, pass in the PEM-encoded certificate and
key
formatters Object Custom response formatters for res.send()
log Object You can optionally pass in a bunyan instance; not required
name String By default, this will be set in the Server response header, default is restify
spdy Object Any options accepted by node-spdy
version String A default version to set for all routes
handleUpgrades Boolean Hook the upgrade event from the node HTTP server, pushing
Connection:
Upgrade requests through the regular request handling chain;
defaults to false
Common handlers: server.use()
A restify server has a use() method that takes handlers of the form
function (req, res,
next) . Note that
restify runs handlers in the order they are registered on a server, so if you want some common handlers to run
before any of your routes, issue calls to use() before defining routes.
Note that in all calls to use() and the routes below, you can pass in any combination of direct functions (
function(res, res, next)) and arrays of functions (
[function(req, res,
next)] ).
Routing
restify routing, in 'basic' mode, is pretty much identical to express/sinatra, in that HTTP verbs are used with a
parameterized resource to determine what chain of handlers to run. Values associated with named placeholders
are available in req.params. Note that values will be URL-decoded before being passed to you.
function send(req, res, next) {
res.send('hello ' + req.params.name);
return next();
}
server.post('/hello', function create(req, res, next) {
res.send(201, Math.random().toString(36).substr(3,
8));
return next();
});
server.put('/hello', send);
server.get('/hello/:name', send);
server.head('/hello/:name', send);
server.del('hello/:name', function rm(req, res, next) {
res.send(204);
return next();
});
You can also pass in a RegExp object and access the capture group with req.params (which will not be
interpreted in any way):
server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next)
{
console.log(req.params[0]);
console.log(req.params[1]);
res.send(200);
return next();
});
Here any request like:
curl
localhost:8080/foo/my/cats/name/is/gandalf
Would result in req.params[0] being foo and req.params[1] being my/cats/name/is/gandalf.
Basically, you can do whatever you want.
Note the use of next(). You are responsible for calling next() in order to run the next handler in the chain. As
below, you can pass an Error object in to have restify automatically return responses to the client.
You can pass in false to not error, but to stop the handler chain. This is useful if you had a res.send in an early
filter, which is not an error, and you possibly have one later you want to short-circuit.
Lastly, you can pass in a string name to next(), and restify will lookup that route, and assuming it exists will run
the chain from where you left off . So for example:
var count = 0;
server.use(function foo(req, res, next) {
count++;
next();
});
server.get('/foo/:id', function (req, res, next)
{
next('foo2');
});
server.get({
name: 'foo2',
path: '/foo/:id'
}, function (req, res, next) {
assert.equal(count, 1);
res.send(200);
next();
});
Note that foo only gets run once in that example. A few caveats:
If you provide a name that doesn't exist, restify will 500 that request.
Don't be silly and call this in cycles. restify won't check that.
Lastly, you cannot "chain" next('route') calls; you can only delegate the routing chain once (this is a
limitation of the way routes are stored internally, and may be revisited someday).
Chaining Handlers
Routes can also accept more than one handler function. For instance:
server.get(
'/foo/:id',
function(req, res, next) {

console.log('Authenticate');
return next();
},
function(req, res, next) {
res.send(200);
return next();
}
);
Hypermedia
If a parameterized route was defined with a string (not a regex), you can render it from other places in the server.
This is useful to have HTTP responses that link to other resources, without having to hardcode URLs throughout
the codebase. Both path and query strings parameters get URL encoded appropriately.
```js server.get({name: 'city', path: '/cities/:slug'}, /* ... */);
// in another route res.send({ country: 'Australia', // render a URL by specifying the route name and parameters
capital: server.router.render('city', {slug: 'canberra'}, {details: true}) }); ```
Which returns:
json { "country": "Australia", "capital": "/cities/canberra?details=true"
}
Versioned Routes
Most REST APIs tend to need versioning, and restify ships with support for semver versioning in an
Accept-Version header, the same way you specify NPM version dependencies:
var restify = require('restify');
var server = restify.createServer();
function sendV1(req, res, next) {
res.send('hello: ' + req.params.name);
return next();
}
function sendV2(req, res, next) {
res.send({hello: req.params.name});
return next();
}
var PATH = '/hello/:name';
server.get({path: PATH, version: '1.1.3'},
sendV1);
server.get({path: PATH, version: '2.0.0'},
sendV2);
server.listen(8080);
Try hitting with:
curl -s localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~1' localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~2' localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~3' localhost:8080/hello/mark |
json
{
"code": "InvalidVersion",
"message": "GET /hello/mark supports versions: 1.1.3, 2.0.0"
}
In the first case, we didn't specify an Accept-Version header at all, so restify treats that like sending a *. Much
as not sending an Accept header means the client gets the server's choice. Restify will choose the first matching
route, in the order specified in the code. In the second case, we explicitly asked for for V1, which got us the same
response, but then we asked for V2 and got back JSON. Finally, we asked for a version that doesn't exist and got
an error (notably, we didn't send an Accept header, so we got a JSON response). Which segues us nicely into
content negotiation.
You can default the versions on routes by passing in a version field at server creation time. Lastly, you can support
multiple versions in the API by using an array:
server.get({path: PATH, version: ['2.0.0', '2.1.0']},
sendV2);
Upgrade Requests
Incoming HTTP requests that contain a
Connection:
Upgrade header are treated somewhat differently by the
node HTTP server. If you want restify to push Upgrade requests through the regular routing chain, you need to
enable handleUpgrades when creating the server.
To determine if a request is eligible for Upgrade, check for the existence of res.claimUpgrade(). This method
will return an object with two properties: the socket of the underlying connection, and the first received data
Buffer as head (may be zero-length).
Once res.claimUpgrade() is called, res itself is marked unusable for further HTTP responses; any later
attempt to send() or end(), etc, will throw an Error. Likewise if res has already been used to send at least part
of a response to the client, res.claimUpgrade() will throw an Error. Upgrades and regular HTTP Response
behaviour are mutually exclusive on any particular connection.
Using the Upgrade mechanism, you can use a library like watershed to negotiate WebSockets connections. For
example:
var ws = new Watershed();
server.get('/websocket/attach', function upgradeRoute(req, res, next)
{
if (!res.claimUpgrade) {
next(new Error('Connection Must Upgrade For WebSockets'));
return;
}
var upgrade = res.claimUpgrade();
var shed = ws.accept(req, upgrade.socket, upgrade.head);
shed.on('text', function(msg) {
console.log('Received message from websocket client: ' + msg);
});
shed.send('hello there!');
next(false);
});
Content Negotiation
If you're using res.send() restify will automatically select the content-type to respond with, by finding the first
registered formatter defined. Note in the examples above we've not defined any formatters, so we've been
leveraging the fact that restify ships with application/json, text/plain and application/octet-stream
formatters. You can add additional formatters to restify by passing in a hash of content-type -> parser at server
creation time:
var server = restify.createServer({
formatters: {
'application/foo': function formatFoo(req, res, body)
{
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
You can do whatever you want, but you probably want to check the type of body to figure out what type it is,
notably for Error/Buffer/everything else. You can always add more formatters later by just setting the formatter on
server.formatters, but it's probably sane to just do it at construct time. Also, note that if a content-type can't
be negotiated, the default is application/octet-stream. Of course, you can always explicitly set the content-
type:
res.setHeader('content-type',
'application/foo');
res.send({hello: 'world'});
Note that there are typically at least three content-types supported by restify: json, text and binary. When you
override or append to this, the "priority" might change; to ensure that the priority is set to what you want, you should
set a q-value on your formatter definitions, which will ensure sorting happens the way you want:
restify.createServer({
formatters: {
'application/foo; q=0.9': function formatFoo(req, res, body)
{
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
Lastly, you don't have to use any of this magic, as a restify response object has all the "raw" methods of a node
ServerResponse on it as well.
var body = 'hello world';
res.writeHead(200, {
'Content-Length':
Buffer.byteLength(body),
'Content-Type': 'text/plain'
});
res.write(body);
res.end();
Error handling
You can handle errors in restify a few different ways. First, you can always just call res.send(err). You can also
shorthand this in a route by doing:
server.get('/hello/:name', function(req, res, next) {
return database.get(req.params.name, function(err, user)
{
if (err)
return next(err);
res.send(user);
return next();
});
});
If you invoke res.send() with an error that has a statusCode attribute, that will be used, otherwise a default of
500 will be used (unless you're using
res.send(4xx, new
Error('blah)) ).
Alternatively, restify 2.1 supports a next.ifError API:
server.get('/hello/:name', function(req, res, next) {
return database.get(req.params.name, function(err, user)
{
next.ifError(err);
res.send(user);
next();
});
});
HttpError
Now the obvious question is what that exactly does (in either case). restify tries to be programmer-friendly with
errors by exposing all HTTP status codes as a subclass of HttpError. So, for example, you can do this:
server.get('/hello/:name', function(req, res, next) {
return next(new restify.ConflictError("I just don't like you"));
});
$ curl -is -H 'accept: text/*' localhost:8080/hello/mark
HTTP/1.1 409 Conflict
Content-Type: text/plain
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5,
Content-Type, Date, Api-Version
Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time
Connection: close
Content-Length: 21
Content-MD5: up6uNh2ejV/C6JUbLlvsiw==
Date: Tue, 03 Jan 2012 00:24:48 GMT
Server: restify
Request-Id: 1685313e-e801-4d90-9537-7ca20a27acfc
Response-Time: 1
I just don't like you
Alternatively, you can access the error classes via restify.errors. We can do this with a simple change to the
previous example:
server.get('/hello/:name', function(req, res, next) {
return next(new restify.errors.ConflictError("I just don't like
you"));
});
The core thing to note about an HttpError is that it has a numeric code (statusCode) and a body. The
statusCode will automatically set the HTTP response status code, and the body attribute by default will be the
message.
All status codes between 400 and 5xx are automatically converted into an HttpError with the name being
'PascalCase' and spaces removed. For the complete list, take a look at the node source.
From that code above
418: I'm a
teapot would be ImATeapotError, as an example.
RestError
Now, a common problem with REST APIs and HTTP is that they often end up needing to overload 400 and 409 to
mean a bunch of different things. There's no real standard on what to do in these cases, but in general you want
machines to be able to (safely) parse these things out, and so restify defines a convention of a RestError. A
RestError is a subclass of one of the particular HttpError types, and additionally sets the body attribute to be a
JS object with the attributes code and message. For example, here's a built-in RestError:
var server = restify.createServer();
server.get('/hello/:name', function(req, res, next) {
return next(new restify.InvalidArgumentError("I just don't like you"));
});
$ curl -is localhost:8080/hello/mark | json
HTTP/1.1 409 Conflict
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5,
Content-Type, Date, Api-Version
Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time
Connection: close
Content-Length: 60
Content-MD5: MpEcO5EQFUZ2MNeUB2VaZg==
Date: Tue, 03 Jan 2012 00:50:21 GMT
Server: restify
Request-Id: bda456dd-2fe4-478d-809c-7d159d58d579
Response-Time: 3
{
"code": "InvalidArgument",
"message": "I just don't like you"
}
The built-in restify errors are:
RestError
BadDigestError
BadMethodError
InternalError
InvalidArgumentError
InvalidContentError
InvalidCredentialsError
InvalidHeaderError
InvalidVersionError
MissingParameterError
NotAuthorizedError
RequestExpiredError
RequestThrottledError
ResourceNotFoundError
WrongAcceptError
You can always add your own by subclassing restify.RestError like:
var restify = require('restify');
var util = require('util');
function MyError(message) {
restify.RestError.call(this, {
restCode: 'MyError',
statusCode: 418,
message: message,
constructorOpt: MyError
});
this.name = 'MyError';
};
util.inherits(MyError,
restify.RestError);
Basically, a RestError takes a statusCode, a restCode, a message, and a "constructorOpt" so that V8 correctly
omits your code from the stack trace (you don't have to do that, but you probably want it). In the example above,
we also set the name property so
console.log(new
MyError()); looks correct.
Socket.IO
To use socket.io with restify, just treat your restify server as if it were a "raw" node server:
var server = restify.createServer();
var io = socketio.listen(server);
server.get('/', function indexHTML(req, res, next) {
fs.readFile(__dirname + '/index.html', function (err, data)
{
if (err) {
next(err);
return;
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(data);
next();
});
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
server.listen(8080, function () {
console.log('socket.io server listening at %s', server.url);
});
Server API
Events
Restify servers emit all the events from the node http.Server and has several other events you want to listen on.
Event: 'NotFound'
function (request, response, cb)
{}
When a client request is sent for a URL that does not exist, restify will emit this event. Note that restify checks for
listeners on this event, and if there are none, responds with a default 404 handler. It is expected that if you listen
for this event, you respond to the client.
Event: 'MethodNotAllowed'
function (request, response, cb)
{}
When a client request is sent for a URL that does exist, but you have not registered a route for that HTTP verb,
restify will emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a
default 405 handler. It is expected that if you listen for this event, you respond to the client.
Event: 'VersionNotAllowed'
function (request, response, cb)
{}
When a client request is sent for a route that exists, but does not match the version(s) on those routes, restify will
emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a default
400 handler. It is expected that if you listen for this event, you respond to the client.
Event: UnsupportedMediaType'
function (request, response, cb)
{}
When a client request is sent for a route that exist, but has a content-type mismatch, restify will emit this event.
Note that restify checks for listeners on this event, and if there are none, responds with a default 415 handler. It is
expected that if you listen for this event, you respond to the client.
Event: 'after'
function (request, response, route, error)
{}
Emitted after a route has finished all the handlers you registered. You can use this to write audit logs, etc. The
route parameter will be the Route object that ran. Note that when you are using the default 404/405/BadVersion
handlers, this event will still be fired, but route will be null. If you have registered your own listeners for those, this
event will not be fired unless you invoke the cb argument that is provided with them.
Event: 'uncaughtException'
function (request, response, route, error)
{}
Emitted when some handler throws an uncaughtException somewhere in the chain. The default behavior is to just
call res.send(error), and let the built-ins in restify handle transforming, but you can override to whatever you
want here.
Properties
A restify server has the following properties on it:
Name Type Description
name String name of the server
version String default version to use in all routes
log Object bunyan instance
acceptable Array(String) list of content-types this server can respond with
url String Once listen() is called, this will be filled in with where the server is running
Other Methods
address()
Wraps node's address().
listen(port, [host], [callback]) or listen(path, [callback])
Wraps node's listen().
close()
Wraps node's close().
pre()
Allows you to add in handlers that run before routing occurs. This gives you a hook to change request headers and
the like if you need to. Note that req.params will be undefined, as that's filled in after routing.
server.pre(function(req, res, next) {
req.headers.accept = 'application/json'; // screw you
client!
return next();
});
use()
Allows you to add in handlers that run no matter what the route.
Bundled Plugins
restify ships with several handlers you can use, specifically:
Accept header parsing
Authorization header parsing
CORS handling plugin
Date header parsing
JSONP support
Gzip Response
Query string parsing
Body parsing (JSON/URL-encoded/multipart form)
Static file serving
Throttling
Conditional request handling
Audit logger
Here's some example code using all the shipped plugins:
var server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.use(restify.bodyParser());
server.use(restify.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'192.168.1.1': {
rate: 0, // unlimited
burst: 0
}
}
}));
server.use(restify.conditionalRequest());
Accept Parser
Parses out the Accept header, and ensures that the server can respond to what the client asked for. You almost
always want to just pass in server.acceptable here, as that's an array of content types the server knows how
to respond to (with the formatters you've registered). If the request is for a non-handled type, this plugin will return
an error of 406.
server.use(restify.acceptParser(server.acceptable));
Authorization Parser
server.use(restify.authorizationParser());
Parses out the Authorization header as best restify can. Currently only HTTP Basic Auth and HTTP Signature
schemes are supported. When this is used, req.authorization will be set to something like:
{
scheme: <Basic|Signature|...>,
credentials: <Undecoded value of
header>,
basic: {
username: $user
password: $password
}
}
req.username will also be set, and defaults to 'anonymous'. If the scheme is unrecognized, the only thing
avaiable in req.authorization will be scheme and credentials - it will be up to you to parse out the rest.
CORS
server.use(restify.CORS());
Supports tacking CORS headers into actual requests (as defined by the spec). Note that preflight requests are
automatically handled by the router, and you can override the default behavior on a per-URL basis with
server.opts(:url, ...). To fully specify this plugin, a sample invocation is:
server.use(restify.CORS({
origins: ['https://foo.com', 'http://bar.com', 'http://baz.com:8081'], //
defaults to ['*']
credentials: true // defaults to false
headers: ['x-foo'] // sets expose-headers
}));
Date Parser
server.use(restify.dateParser());
Parses out the HTTP Date header (if present) and checks for clock skew (default allowed clock skew is 300s, like
Kerberos). You can pass in a number, which is interpreted in seconds, to allow for clock skew.
// Allows clock skew of 1m
server.use(restify.dateParser(60));
QueryParser
server.use(restify.queryParser());
Parses the HTTP query string (i.e., /foo?id=bar&name=mark). If you use this, the parsed content will always be
available in req.query, additionally params are merged into req.params. You can disable by passing in
mapParams: false in the options object:
server.use(restify.queryParser({ mapParams: false
}));
JSONP
Supports checking the query string for callback or jsonp and ensuring that the content-type is appropriately set
if JSONP params are in place. There is also a default application/javascript formatter to handle this.
You should set the queryParser plugin to run before this, but if you don't this plugin will still parse the query string
properly.
BodyParser
Blocks your chain on reading and parsing the HTTP request body. Switches on Content-Type and does the
appropriate logic. application/json, application/x-www-form-urlencoded and
multipart/form-data are currently supported.
server.use(restify.bodyParser({
maxBodySize: 0,
mapParams: true,
mapFiles: false,
overrideParams: false,
multipartHandler: function(part) {
part.on('data', function(data) {
/* do something with the multipart data */
});
},
multipartFileHandler: function(part) {
part.on('data', function(data) {
/* do something with the multipart file data
*/
});
},
keepExtensions: false,
uploadDir: os.tmpdir()
}));
Options:
maxBodySize - The maximum size in bytes allowed in the HTTP body. Useful for limiting clients from
hogging server memory.
mapParams - if req.params should be filled with parsed parameters from HTTP body.
mapFiles - if req.params should be filled with the contents of files sent through a multipart request.
formidable is used internally for parsing, and a file is denoted as a multipart part with the filename option
set in its Content-Disposition. This will only be performed if mapParams is true.
overrideParams - if an entry in req.params should be overwritten by the value in the body if the names
are the same. For instance, if you have the route /:someval, and someone posts an
x-www-form-urlencoded Content-Type with the body someval=happy to /sad, the value will be
happy if overrideParams is true, sad otherwise.
multipartHandler - a callback to handle any multipart part which is not a file. If this is omitted, the
default handler is invoked which may or may not map the parts into req.params, depending on the
mapParams-option.
multipartFileHandler - a callback to handle any multipart file. It will be a file if the part have a
Content-Disposition with the filename parameter set. This typically happens when a browser sends
a from and there is a parameter similar to
<input type="file"
/> . If this is not provided, the default
behaviour is to map the contents into req.params.
keepExtensions - if you want the uploaded files to include the extensions of the original files (multipart
uploads only). Does nothing if multipartFileHandler is defined.
uploadDir - Where uploaded files are intermediately stored during transfer before the contents is mapped
into req.params. Does nothing if multipartFileHandler is defined.
RequestLogger
Sets up a child bunyan logger with the current request id filled in, along with any other parameters you define.
server.use(restify.requestLogger({
properties: {
foo: 'bar'
},
serializers: {...}
}));
You can pass in no options to this, in which case only the request id will be appended, and no serializers appended
(this is also the most performant); the logger created at server creation time will be used as the parent logger.
GzipResponse
server.use(restify.gzipResponse());
If the client sends an accept-encoding: gzip header (or one with an appropriate q-val), then the server will
automatically gzip all response data. Note that only gzip is supported, as this is most widely supported by clients
in the wild. This plugin will overwrite some of the internal streams, so any calls to res.send, res.write, etc., will
be compressed. A side effect is that the content-length header cannot be known, and so
transfer-encoding: chunked will always be set when this is in effect. This plugin has no impact if the client
does not send accept-encoding: gzip.
Serve Static
The serveStatic module is different than most of the other plugins, in that it is expected that you are going to map it
to a route, as below:
server.get(/\/docs\/current\/?.*/,
restify.serveStatic({
directory: './documentation/v1',
default: 'index.html'
}));
The above route and directory combination will serve a file located in
./documentation/v1/docs/current/index.html when you attempt to hit
http://localhost:8080/docs/current/.
The plugin will enforce that all files under directory are served. The directory served is relative to the
process working directory. You can also provide a default parameter such as index.html for any directory that
lacks a direct file match. You can specify additional restrictions by passing in a match parameter, which is just a
RegExp to check against the requested file name. Lastly, you can pass in a maxAge numeric, which will set the
Cache-Control header. Default is 3600 (1 hour).
Throttle
restify ships with a fairly comprehensive implementation of Token bucket, with the ability to throttle on IP (or x-
forwarded-for) and username (from req.username). You define "global" request rate and burst rate, and you can
define overrides for specific keys. Note that you can always place this on per-URL routes to enable different
request rates to different resources (if for example, one route, like /my/slow/database is much easier to
overwhlem than /my/fast/memcache).
server.use(restify.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'192.168.1.1': {
rate: 0, //
unlimited
burst: 0
}
}
}));
If a client has consumed all of their available rate/burst, an HTTP response code of 429 Too Many Requests is
returned.
Options:
Name Type Description
rate Number Steady state number of requests/second to allow
burst Number If available, the amount of requests to burst to
ip Boolean Do throttling on a /32 (source IP)
xff Boolean Do throttling on a /32 (X-Forwarded-For)
username Boolean Do throttling on req.username
overrides Object Per "key" overrides
tokensTable Object Storage engine; must support put/get
maxKeys Number If using the built-in storage table, the maximum distinct throttling keys to allow at a time
Note that ip, xff and username are XOR'd.
Using an external storage mechanism for key/bucket mappings.
By default, the restify throttling plugin uses an in-memory LRU to store mappings between throttling keys (i.e., IP
address) to the actual bucket that key is consuming. If this suits you, you can tune the maximum number of keys to
store in memory with options.maxKeys; the default is 10000.
In some circumstances, you want to offload this into a shared system, such as Redis, if you have a fleet of API
servers and you're not getting steady and/or uniform request distribution. To enable this, you can pass in
options.tokensTable, which is simply any Object that supports put and get with a String key, and an
Object value.
Conditional Request Handler
server.use(restify.conditionalRequest());
You can use this handler to let clients do nice HTTP semantics with the "match" headers. Specifically, with this
plugin in place, you would set res.etag=$yourhashhere, and then this plugin will do one of:
return 304 (Not Modified) [and stop the handler chain]
return 412 (Precondition Failed) [and stop the handler chain]
Allow the request to go through the handler chain.
The specific headers this plugin looks at are:
Last-Modified
If-Match
If-None-Match
If-Modified-Since
If-Unmodified-Since
Some example usage:
server.use(function setETag(req, res, next) {
res.header('ETag', 'myETag');
res.header('Last-Modified', new Date());
});
server.use(restify.conditionalRequest());
server.get('/hello/:name', function(req, res, next)
{
res.send('hello ' + req.params.name);
});
Audit Logging
Audit logging is a special plugin, as you don't use it with .use(), but with the after event:
server.on('after', restify.auditLogger({
log: bunyan.createLogger({
name: 'audit',
stream: process.stdout
})
}));
You pass in the auditor a bunyan logger, and it will write out records at the info level. Records will look like this:
{
"name": "audit",
"hostname": "your.host.name",
"audit": true,
"remoteAddress": "127.0.0.1",
"remotePort": 57692,
"req_id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
"req": {
"method": "GET",
"url": "/foo",
"headers": {
"authorization": "Basic YWRtaW46am95cGFzczEyMw==",
"user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7
OpenSSL/0.9.8r zlib/1.2.3",
"host": "localhost:8080",
"accept": "application/json"
},
"httpVersion": "1.1",
"trailers": {},
"version": "*"
},
"res": {
"statusCode": 200,
"headers": {
"access-control-allow-origin": "*",
"access-control-allow-headers": "Accept, Accept-Version, Content-Length,
Content-MD5, Content-Type, Date, Api-Version",
"access-control-expose-headers": "Api-Version, Request-Id, Response-
Time",
"server": "Joyent SmartDataCenter 7.0.0",
"x-request-id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
"access-control-allow-methods": "GET",
"x-api-version": "1.0.0",
"connection": "close",
"content-length": 158,
"content-md5": "zkiRn2/k3saflPhxXI7aXA==",
"content-type": "application/json",
"date": "Tue, 07 Feb 2012 20:30:31 GMT",
"x-response-time": 1639
},
"trailer": false
},
"route": {
"name": "GetFoo",
"version": ["1.0.0"]
},
"secure": false,
"level": 30,
"msg": GetFoo handled: 200",
"time": "2012-02-07T20:30:31.896Z",
"v": 0
}
Request API
Wraps all of the node http.IncomingMessage APIs, events and properties, plus the following.
header(key, [defaultValue])
Get the case-insensitive request header key, and optionally provide a default value (express-compliant):
req.header('Host');
req.header('HOST');
req.header('Accept', '*/*');
accepts(type)
(express-compliant)
Check if the Accept header is present, and includes the given type.
When the Accept header is not present true is returned. Otherwise the given type is matched by an exact match,
and then subtypes. You may pass the subtype such as html which is then converted internally to text/html
using the mime lookup table.
// Accept: text/html
req.accepts('html');
// => true
// Accept: text/*;
application/json
req.accepts('html');
req.accepts('text/html');
req.accepts('text/plain');
req.accepts('application/json');
// => true
req.accepts('image/png');
req.accepts('png');
// => false
is(type)
Check if the incoming request contains the Content-Type header field, and it contains the give mime type.
// With Content-Type: text/html; charset=utf-
8
req.is('html');
req.is('text/html');
// => true
// When Content-Type is application/json
req.is('json');
req.is('application/json');
// => true
req.is('html');
// => false
Note this is almost compliant with express, but restify does not have all the app.is() callback business express
does.
isSecure()
Check if the incoming request is encrypted.
isChunked()
Check if the incoming request is chunked.
isKeepAlive()
Check if the incoming request is kept alive.
log
Note that you can piggyback on the restify logging framework, by just using req.log. I.e.,:
function myHandler(req, res, next) {
var log = req.log;
log.debug({params: req.params}, 'Hello there %s',
'foo');
}
The advantage to doing this is that each restify req instance has a new bunyan instance log on it where the
request id is automatically injected in, so you can easily correlate your high-throughput logs together.
getLogger(component)
Shorthand to grab a new bunyan instance that is a child component of the one restify has:
var log =
req.getLogger('MyFoo');
time()
the time when this request arrived (ms since epoch)
Properties
Name Type Description
contentLength Number short hand for the header content-length
contentType String short hand for the header content-type
href String url.parse(req.url) href
log Object bunyan logger you can piggyback on
id String A unique request id (x-request-id)
path String cleaned up URL path
Response API
Wraps all of the node ServerResponse APIs, events and properties, plus the following.
header(key, value)
Get or set the response header key.
res.header('Content-Length');
// => undefined
res.header('Content-Length', 123);
// => 123
res.header('Content-Length');
// => 123
res.header('foo', new Date());
// => Fri, 03 Feb 2012 20:09:58
GMT
charSet(type)
Appends the provided character set to the response's Content-Type.
res.charSet('utf-8');
Will change the normal json Content-Type to application/json; charset=utf-8.
cache([type], [options])
Sets the cache-control header. type defaults to _public_, and options currently only takes maxAge.
res.cache();
status(code)
Sets the response statusCode.
res.status(201);
send([status], body)
You can use send() to wrap up all the usual writeHead(), write(), end() calls on the HTTP API of node.
You can pass send either a code and body, or just a body. body can be an Object, a Buffer, or an Error. When you
call send(), restify figures out how to format the response (see content-negotiation, above), and does that.
res.send({hello: 'world'});
res.send(201, {hello: 'world'});
res.send(new
BadRequestError('meh'));
json([status], body)
Short-hand for:
res.contentType = 'json';
res.send({hello:
'world'});
Properties
Name Type Description
code Number HTTP status code
contentLength Number short hand for the header content-length
contentType String short hand for the header content-type
headers Object response headers
id String A unique request id (x-request-id)
Setting the default headers
You can change what headers restify sends by default by setting the top-level property
defaultResponseHeaders. This should be a function that takes one argument data, which is the already
serialized response body. data can be either a String or Buffer (or null). The this object will be the response
itself.
var restify = require('restify');
restify.defaultResponseHeaders = function(data) {
this.header('Server', 'helloworld');
};
restify.defaultResponseHeaders = false; // disable
altogether
DTrace
One of the coolest features of restify is that it automatically creates DTrace probes for you whenever you add a new
route/handler. The easiest way to explain this is with an example:
var restify = require('restify');
var server = restify.createServer({
name: 'helloworld'
});
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.urlEncodedBodyParser());
server.use(function slowHandler(req, res, next) {
setTimeout(function() {
return next();
}, 250);
});
server.get({path: '/hello/:name', name: 'GetFoo'}, function respond(req, res, next)
{
res.send({
hello: req.params.name
});
return next();
});
server.listen(8080, function() {
console.log('listening: %s', server.url);
});
So we've got our typical "hello world" server now, with a slight twist; we introduced an artificial 250ms lag. Also,
note that we named our server, our routes, and all of our handlers (functions); while that's optional, it does make
DTrace much more usable. So, if you started that server, then looked for DTrace probes, you'd see something like
this:
dtrace -l -P restify*
ID PROVIDER MODULE FUNCTION NAME
24 restify38789 mod-88f3f88 route-start route-
start
25 restify38789 mod-88f3f88 handler-start handler-
start
26 restify38789 mod-88f3f88 handler-done handler-
done
27 restify38789 mod-88f3f88 route-done route-
done
route-start
Field Type Description
server name char * name of the restify server that fired
route name char * name of the route that fired
id int unique id for this request
method char * HTTP request method
url char * (full) HTTP URL
headers char * JSON encoded map of all request headers
handler-start
Field Type Description
server name char * name of the restify server that fired
route name char * name of the route that fired
handler name char * name of the function that just entered
id int unique id for this request
route-done
Field Type Description
server name char * name of the restify server that fired
route name char * name of the route that fired
id int unique id for this request
statusCode int HTTP response code
headers char * JSON encoded map of response headers
handler-done
Field Type Description
server name char * name of the restify server that fired
route name char * name of the route that fired
handler name char * name of the function that just entered
id int unique id for this request
Example D Script
Now, if you wanted to say get a breakdown of latency by handler, you could do something like this:
#!/usr/sbin/dtrace -s
#pragma D option quiet
restify*:::route-start
{
track[arg2] = timestamp;
}
restify*:::handler-start
/track[arg3]/
{
h[arg3, copyinstr(arg2)] = timestamp;
}
restify*:::handler-done
/track[arg3] && h[arg3, copyinstr(arg2)]/
{
@[copyinstr(arg2)] = quantize((timestamp - h[arg3, copyinstr(arg2)]) /
1000000);
h[arg3, copyinstr(arg2)] = 0;
}
restify*:::route-done
/track[arg2]/
{
@[copyinstr(arg1)] = quantize((timestamp - track[arg2]) / 1000000);
track[arg2] = 0;
}
So running the server in one terminal:
node helloworld.js
The D script in another:
./helloworld.d
Hit the server a few times with curl:
for i in {1..10} ; do curl -is http://127.0.0.1:8080/hello/mark ;
done
Then Ctrl-C the D script, and you'll see the "slowHandler" at the bottom of the stack, bucketized that it's the vast
majority of latency in this pipeline
handler-6
value ------------- Distribution -------------
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseAccept
value ------------- Distribution -------------
count
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseAuthorization
value ------------- Distribution -------------
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseDate
value ------------- Distribution -------------
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseQueryString
value ------------- Distribution -------------
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseUrlEncodedBody
value ------------- Distribution -------------
count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
respond
value ------------- Distribution -------------
count
1 | 0
2 |@@@@ 1
4 | 0
8 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
16 | 0
slowHandler
value ------------- Distribution -------------
count
64 | 0
128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
256 |@@@@ 1
512 | 0
getfoo
value ------------- Distribution -------------
count
64 | 0
128 |@@@@ 1
256 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
512 |
Client API
There are actually three separate clients shipped in restify:
JsonClient: sends and expects application/json
StringClient: sends url-encoded request and expects text/plain
HttpClient: thin wrapper over node's http/https libraries
The idea being that if you want to support "typical" control-plane REST APIs, you probably want the JsonClient,
or if you're using some other serialization (like XML) you'd write your own client that extends the StringClient. If
you need streaming support, you'll need to do some work on top of the HttpClient, as StringClient and
friends buffer requests/responses.
All clients support retry with exponential backoff for getting a TCP connection; they do not perform retries on 5xx
error codes like previous versions of the restify client. You can set retry to false to disable this logic altogether.
Also, all clients support a connectTimeout field, which is use on each retry. The default is not to set a
connectTimeout, so you end up with the node.js socket defaults.
Here's an example of hitting the Joyent CloudAPI:
var restify = require('restify');
// Creates a JSON client
var client = restify.createJsonClient({
url: 'https://us-west-1.api.joyentcloud.com'
});
client.basicAuth('$login', '$password');
client.get('/my/machines', function(err, req, res, obj)
{
assert.ifError(err);
console.log(JSON.stringify(obj, null, 2));
});
As a short-hand, a client can be initialized with a string-URL rather than an options object:
var restify = require('restify');
var client = restify.createJsonClient('https://us-west-
1.api.joyentcloud.com');
Note that all further documentation refers to the "short-hand" form of methods like get/put/del which take a
string path. You can also pass in an object to any of those methods with extra params (notably headers):
var options = {
path: '/foo/bar',
headers: {
'x-foo': 'bar'
},
retry: {
'retries': 0
},
agent: false
};
client.get(options, function(err, req, res) { ..
});
If you need to interpose additional headers in the request before it is sent on to the server, you can provide a
synchronous callback function as the signRequest option when creating a client. This is particularly useful with
node-http-signature, which needs to attach a cryptographic signature of selected outgoing headers. If provided,
this callback will be invoked with a single parameter: the outgoing http.ClientRequest object.
JsonClient
The JSON Client is the highest-level client bundled with restify; it exports a set of methods that map directly to
HTTP verbs. All callbacks look like
function(err, req, res,
[obj]) , where obj is optional, depending on
if content was returned. HTTP status codes are not interpreted, so if the server returned 4xx or something with a
JSON payload, obj will be the JSON payload. err however will be set if the server returned a status code >= 400
(it will be one of the restify HTTP errors). If obj looks like a RestError:
{
"code": "FooError",
"message": "some foo
happened"
}
then err gets "upconverted" into a RestError for you. Otherwise it will be an HttpError.
createJsonClient(options)
var client = restify.createJsonClient({
url: 'https://api.us-west-
1.joyentcloud.com',
version: '*'
});
Options:
Name Type Description
accept String Accept header to send
connectTimeout Number Amount of time to wait for a socket
requestTimeout Number Amount of time to wait for the request to finish
dtrace Object node-dtrace-provider handle
gzip Object Will compress data when sent using content-encoding: gzip
headers Object HTTP headers to set in all requests
log Object bunyan instance
retry Object options to provide to node-retry;"false" disables retry; defaults to 4 retries
signRequest Function synchronous callback for interposing headers before request is sent
url String Fully-qualified URL to connect to
userAgent String user-agent string to use; restify inserts one, but you can override it
version String semver string to set the accept-version
get(path, callback)
Performs an HTTP get; if no payload was returned, obj defaults to {} for you (so you don't get a bunch of null
pointer errors).
client.get('/foo/bar', function(err, req, res, obj)
{
assert.ifError(err);
console.log('%j', obj);
});
head(path, callback)
Just like get, but without obj:
client.head('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode,
res.headers);
});
post(path, object, callback)
Takes a complete object to serialize and send to the server.
client.post('/foo', { hello: 'world' }, function(err, req, res, obj)
{
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%j', obj);
});
put(path, object, callback)
Just like post:
client.put('/foo', { hello: 'world' }, function(err, req, res, obj)
{
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%j', obj);
});
del(path, callback)
del doesn't take content, since you know, it should't:
client.del('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode,
res.headers);
});
StringClient
StringClient is what JsonClient is built on, and provides a base for you to write other buffering/parsing
clients (like say an XML client). If you need to talk to some "raw" HTTP server, then StringClient is what you
want, as it by default will provide you with content uploads in application/x-www-form-url-encoded and
downloads as text/plain. To extend a StringClient, take a look at the source for JsonClient. Effectively,
you extend it, and set the appropriate options in the constructor and implement a write (for put/post) and parse
method (for all HTTP bodies), and that's it.
createStringClient(options)
var client =
restify.createStringClient({
url: 'https://example.com'
})
get(path, callback)
Performs an HTTP get; if no payload was returned, data defaults to '' for you (so you don't get a bunch of null
pointer errors).
client.get('/foo/bar', function(err, req, res, data)
{
assert.ifError(err);
console.log('%s', data);
});
head(path, callback)
Just like get, but without data:
client.head('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode,
res.headers);
});
post(path, object, callback)
Takes a complete object to serialize and send to the server.
client.post('/foo', { hello: 'world' }, function(err, req, res, data)
{
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%s', data);
});
put(path, object, callback)
Just like post:
client.put('/foo', { hello: 'world' }, function(err, req, res, data)
{
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%s', data);
});
del(path, callback)
del doesn't take content, since you know, it should't:
client.del('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode,
res.headers);
});
HttpClient
HttpClient is the lowest-level client shipped in restify, and is basically just some sugar over the top of node's
http/https modules (with HTTP methods like the other clients). It is useful if you want to stream with restify. Note
that the event below is unfortunately named result and not response (because Event 'response' is already
used).
client = restify.createClient({
url: 'http://127.0.0.1'
});
client.get('/str/mcavage', function(err, req) {
assert.ifError(err); // connection error
req.on('result', function(err, res) {
assert.ifError(err); // HTTP status code >=
400
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
console.log(body);
});
});
});
Or a write:
client.post(opts, function(err, req) {
assert.ifError(connectErr);
req.on('result', function(err, res)
{
assert.ifError(err);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
console.log(res.body);
});
});
req.write('hello world');
req.end();
});
Note that get/head/del all call req.end() for you, so you can't write data over those. Otherwise, all the same
methods exist as JsonClient/StringClient.
One wishing to extend the HttpClient should look at the internals and note that read and write probably need
to be overridden.
basicAuth(username, password)
Since it hasn't been mentioned yet, this convenience method (available on all clients), just sets the
Authorization header for all HTTP requests:
client.basicAuth('mark', 'mysupersecretpassword');
Upgrades
If you successfully negotiate an Upgrade with the HTTP server, an upgradeResult event will be emitted with the
arguments err, res, socket and head. You can use this functionality to establish a WebSockets connection with
a server. For example, using the watershed library:
var ws = new Watershed();
var wskey = ws.generateKey();
var options = {
path: '/websockets/attach',
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey,
}
};
client.get(options, function(err, res, socket, head) {
req.once('upgradeResult', function(err, res, socket, head)
{
var shed = ws.connect(res, socket, head, wskey);
shed.on('text', function(msg) {
console.log('message from server: ' + msg);
shed.end();
});
shed.send('greetings program');
});
});

Anda mungkin juga menyukai