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.
For real time chat, discussion and support, join the #restify
IRC channel on
irc.freenode.net
.
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
restify.createServercreateServer
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'),
fs = require('fs');
var server = restify.createServer({
certificate: fs.readFileSync('path/to/server/certificate'),
key: fs.readFileSync('path/to/server/key'),
name: 'MyApp',
});
server.listen(8080);
Option | Type | Description |
server.certificatecertificate | String | If you want to create an HTTPS server, pass in the path to PEM-encoded certificate and key |
server.keykey | String | If you want to create an HTTPS server, pass in the path to PEM-encoded certificate and key |
server.formattersformatters | Object | Custom response formatters for res.send() |
server.loglog | Object | You can optionally pass in a bunyan instance; not required |
server.namename | String | By default, this will be set in the Server response header, default is restify |
server.spdyspdy | Object | Any options accepted by node-spdy |
server.versionversion | String|Array | A default version to set for all routes |
server.handleUpgradeshandleUpgrades | Boolean | Hook the upgrade event from the node HTTP server, pushing Connection: Upgrade requests through the regular request handling chain; defaults to false |
server.httpsServerOptionshttpsServerOptions | Object | Any options accepted by node-https Server. If provided the following restify server options will be ignored: spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and ciphers; however these can all be specified on httpsServerOptions. |
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)]
).
server.head(res, res, next) server.get(res, res, next) server.put(res, res, next) server.post(res, res, next) server.del(res, 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.
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:
{
"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 this highest matching route.
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', '2.2.0']}, sendV2);
In this case you may need to know more information such as what the original requested version string was, and what the matching version from the routes supported version array was. Two methods make this info available:
var PATH = '/version/test';
server.get({path: PATH, version: ['2.0.0', '2.1.0', '2.2.0']}, function (req) {
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
});
Hitting this route:
curl -s -H 'accept-version: <2.2.0' localhost:8080/version/test | json
{
"requestedVersion": "<2.2.0",
"matchedVersion": "2.1.0"
}
In response to the above
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, cb) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return cb(null, body.toString('base64'));
return cb(null, 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, cb) {
if (body instanceof Error)
return cb(body);
if (Buffer.isBuffer(body))
return cb(null, body.toString('base64'));
return cb(null, 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();
});
});
Sometimes, for all requests, you may want to handle an error condition the same
way. As an example, you may want to serve a 500 page on all
InternalServerErrors
. In this case, you can add a listener for the
InternalServer
error event that is always fired when this Error is
encountered by Restify as part of a next(error)
statement. This gives you a
way to handle all errors of the same class identically across the server. You
can also use a generic restifyError
event which will catch errors of all types.
server.get('/hello/:name', function(req, res, next) {
// some internal unrecoverable error
var err = new restify.errors.InternalServerError('oh noes!');
return next(err);
});
server.get('/hello/:foo', function(req, res, next) {
// resource not found error
var err = new restify.errors.NotFoundError('oh noes!');
return next(err);
});
server.on('NotFound', function (req, res, err, cb) {
// do not call res.send! you are now in an error context and are outside
// of the normal next chain. you can log or do metrics here, and invoke
// the callback when you're done. restify will automtically render the
// NotFoundError as a JSON response.
return cb();
});
server.on('InternalServer', function (req, res, err, cb) {
// if you don't want restify to automatically render the Error object
// as a JSON response, you can customize the response by setting the
// `body` property of the error
err.body = '<html><body>some custom error content!</body></html>';
return cb();
});
server.on('restifyError', function (req, res, err, cb) {
// this listener will fire after both events above!
// `err` here is the same as the error that was passed to the above
// error handlers.
return cb();
});
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:
- BadRequestError (400 Bad Request)
- UnauthorizedError (401 Unauthorized)
- PaymentRequiredError (402 Payment Required)
- ForbiddenError (403 Forbidden)
- NotFoundError (404 Not Found)
- MethodNotAllowedError (405 Method Not Allowed)
- NotAcceptableError (406 Not Acceptable)
- ProxyAuthenticationRequiredError (407 Proxy Authentication Required)
- RequestTimeoutError (408 Request Time-out)
- ConflictError (409 Conflict)
- GoneError (410 Gone)
- LengthRequiredError (411 Length Required)
- PreconditionFailedError (412 Precondition Failed)
- RequestEntityTooLargeError (413 Request Entity Too Large)
- RequesturiTooLargeError (414 Request-URI Too Large)
- UnsupportedMediaTypeError (415 Unsupported Media Type)
- RequestedRangeNotSatisfiableError (416 Requested Range Not Satisfiable)
- ExpectationFailedError (417 Expectation Failed)
- ImATeapotError (418 I'm a teapot)
- UnprocessableEntityError (422 Unprocessable Entity)
- LockedError (423 Locked)
- FailedDependencyError (424 Failed Dependency)
- UnorderedCollectionError (425 Unordered Collection)
- UpgradeRequiredError (426 Upgrade Required)
- PreconditionRequiredError (428 Precondition Required)
- TooManyRequestsError (429 Too Many Requests)
- RequestHeaderFieldsTooLargeError (431 Request Header Fields Too Large)
- InternalServerError (500 Internal Server Error)
- NotImplementedError (501 Not Implemented)
- BadGatewayError (502 Bad Gateway)
- ServiceUnavailableError (503 Service Unavailable)
- GatewayTimeoutError (504 Gateway Time-out)
- HttpVersionNotSupportedError (505 HTTP Version Not Supported)
- VariantAlsoNegotiatesError (506 Variant Also Negotiates)
- InsufficientStorageError (507 Insufficient Storage)
- BandwidthLimitExceededError (509 Bandwidth Limit Exceeded)
- NotExtendedError (510 Not Extended)
- NetworkAuthenticationRequiredError (511 Network Authentication Required)
- BadDigestError (400 Bad Request)
- BadMethodError (405 Method Not Allowed)
- InternalError (500 Internal Server Error)
- InvalidArgumentError (409 Conflict)
- InvalidContentError (400 Bad Request)
- InvalidCredentialsError (401 Unauthorized)
- InvalidHeaderError (400 Bad Request)
- InvalidVersionError (400 Bad Request)
- MissingParameterError (409 Conflict)
- NotAuthorizedError (403 Forbidden)
- RequestExpiredError (400 Bad Request)
- RequestThrottledError (429 Too Many Requests)
- ResourceNotFoundError (404 Not Found)
- WrongAcceptError (406 Not Acceptable)
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);
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
EventsEvents
Restify servers emit all the events from the node http.Server and has several other events you want to listen on.
NotFoundEvent: 'NotFound'
function (request, response, error, 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.
MethodNotAllowedEvent: 'MethodNotAllowed'
function (request, response, error, 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.
VersionNotAllowedEvent: 'VersionNotAllowed'
function (request, response, error, 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.
UnsupportedMediaTypeEvent: 'UnsupportedMediaType'
function (request, response, error, 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.
afterEvent: '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.
uncaughtExceptionEvent: '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 |
server.namename | String | name of the server |
server.versionversion | String|Array | default version to use in all routes |
server.loglog | Object | bunyan instance |
server.acceptableacceptable | Array(String) | list of content-types this server can respond with |
server.urlurl | String | Once listen() is called, this will be filled in with where the server is running |
Other Methods
server.address() server.address()
Wraps node's address().
server.listen()server.listen(port, [host], [callback]) or server.listen(path, [callback])
Wraps node's listen().
server.close()server.close()
Wraps node's close().
server.pre()server.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();
});
You can also clean up URLs before routes are matched with the built in
restify.pre.sanitizePath
. This re-implements v1.4 behaviour
server.pre(restify.pre.sanitizePath());
server.use()server.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.requestExpiry());
server.use(restify.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'192.168.1.1': {
rate: 0, // unlimited
burst: 0
}
}
}));
server.use(restify.conditionalRequest());
restify.acceptParserAccept 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));
restify.authorizationParserAuthorization 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 available in req.authorization
will be
scheme
and credentials
- it will be up to you to parse out the rest.
restify.CORSCORS
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
}));
restify.dateParserDate 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));
restify.queryParserQueryParser
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 }));
restify.jsonpJSONP
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.
restify.bodyParserBodyParser
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(),
multiples: true
hash: 'sha1'
}));
Options:
maxBodySize
- The maximum size in bytes allowed in the HTTP body. Useful for limiting clients from hogging server memory.mapParams
- ifreq.params
should be filled with parsed parameters from HTTP body.mapFiles
- ifreq.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 thefilename
option set in itsContent-Disposition
. This will only be performed ifmapParams
is true.overrideParams
- if an entry inreq.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 anx-www-form-urlencoded
Content-Type with the bodysomeval=happy
to/sad
, the value will behappy
ifoverrideParams
istrue
,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 intoreq.params
, depending on themapParams
-option.multipartFileHandler
- a callback to handle any multipart file. It will be a file if the part have aContent-Disposition
with thefilename
parameter set. This typically happens when a browser sends a form and there is a parameter similar to<input type="file" />
. If this is not provided, the default behaviour is to map the contents intoreq.params
.keepExtensions
- if you want the uploaded files to include the extensions of the original files (multipart uploads only). Does nothing ifmultipartFileHandler
is defined.uploadDir
- Where uploaded files are intermediately stored during transfer before the contents is mapped intoreq.params
. Does nothing ifmultipartFileHandler
is defined.multiples
- if you want to support html5 multiple attribute in upload fields.hash
- If you want checksums calculated for incoming files, set this to eithersha1
ormd5
.
restify.requestLoggerRequestLogger
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. This logger can be used normally, with req.log.
This plugin does _not_ log each individual request. Use the Audit Logging plugin or a custom middleware for that use.
Options:
* headers
- A list of headers to transfer from the request to top level
props on the log.
restify.gzipResponseGzipResponse
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
.
restify.serveStaticServe 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).
An additional option for serving a static file is to pass file
in to the
serveStatic method as an option. The following will serve index.html from
the documentation/v1/ directory anytime a client requests /home/
.
server.get(/\/home\//, restify.serveStatic({
directory: './documentation/v1',
file: 'index.html'
}));
restify.trottleThrottle
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.
restify.requestExpiryRequest Expiry
Request Expiry can be used to throttle requests that have already exceeded their client timeouts. Requests can be sent with a configurable client timeout header, e.g. 'x-request-expiry-time', which gives in absolute ms since epoch, when this request will be timed out by the client.
This plugin will throttle all incoming requests via a 504 where 'x-request-expiry-time' < Date.now() -- since these incoming requests have already been timed out by the client. This prevents the server from processing unnecessary requests.
server.use(restify.requestExpiry({
header: 'x-request-expiry-time'
});
The only option provided is header
which is the request header used to specify
the client timeout.
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.
restify.conditionalRequestConditional 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);
});
restify.AuditLoggerAudit 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
}),
server: SERVER,
logMetrics : logBuffer,
printLog : true
}));
You pass in the auditor a bunyan logger, optionally server object, Ringbuffer and a flag printLog indicate if log needs to be print out at info level or not. By default, without specify printLog flag, it will write out record lookling 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",
"query": {
foo: "bar"
},
"trailers": {},
"version": "*",
"timers": {
"bunyan": 52,
"saveAction": 8,
"reqResTracker": 213,
"addContext": 8,
"addModels": 4,
"resNamespaces": 5,
"parseQueryString": 11,
"instanceHeaders": 20,
"xForwardedProto": 7,
"httpsRedirector": 14,
"readBody": 21,
"parseBody": 6,
"xframe": 7,
"restifyCookieParser": 15,
"fooHandler": 23,
"barHandler": 14,
"carHandler": 14
}
},
"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
}
The timers
field shows the time each handler took to run in microseconds.
Restify by default will record this information for every handler for each
route. However, if you decide to include nested handlers, you can track the
timing yourself by utilizing the Request
startHandlerTimer and
endHandlerTimer API.
You can also listen to auditlog event and get same above log object when log event emits. For example SERVER.on('auditlog', function (data) { //do some process with log }); Log is also accumulated in the Ringbuffer object, if user choose to pass in during auditlogger construction time.
Request API
Wraps all of the node http.IncomingMessage APIs, events and properties, plus the following.
req.header(key, [defaultValue])req.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', '*/*');
req.accepts(type)req.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
req.is(type)req.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.
req.isSecure()req.isSecure()
Check if the incoming request is encrypted.
req.isChunked()req.isChunked()
Check if the incoming request is chunked.
req.isKeepAlive()req.isKeepAlive()
Check if the incoming request is kept alive.
req.logreq.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.
req.getLogger(component)req.getLogger(component)
Shorthand to grab a new bunyan instance that is a child component of the one restify has:
var log = req.getLogger('MyFoo');
req.getQuery()req.getQuery()
Returns the raw query string. Returns empty string if no query string is found.
To get the parsed query object, use the queryParser
plugin. More info can
be found about the plugin in the bundled plugins section.
req.time()req.time()
The time when this request arrived (ms since epoch)
req.startHandlerTimer(handlerName)req.startHandlerTimer(handlerName)
Start the timer for a request handler. You might want to use this if you've got a Restify request handler in your chain that contains nested handlers.
function fooHandler(req, res, next) {
vasync.pipeline(funcs: [
function nestedHandler1(req, res, next) {
req.startHandlerTimer('nestedHandler1');
// do something
req.endHandlerTimer('nestedHandler1');
return next();
},
function nestedHandler1(req, res, next) {
req.startHandlerTimer('nestedHandler2');
// do something
req.endHandlerTimer('nestedHandler2');
return next();
}...
]...
}
req.endHandlerTimer(handlerName)req.endHandlerTimer(handlerName)
End the timer for a request handler. You must invoke this function if you
called startRequestHandler
on a handler. Otherwise the time recorded will be
incorrect.
Properties
Response API
Wraps all of the node ServerResponse APIs, events and properties, plus the following.
res.header(key, value)res.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
res.charSet(type)res.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
.
res.cache([type], [options])res.cache([type], [options])
Sets the cache-control header. type
defaults to _public_, and
options currently only takes maxAge
.
res.cache();
res.status(code)res.status(code)
Sets the response statusCode.
res.status(201);
res.send([status], body)res.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'));
res.redirect(status, url, next)res.redirect(status, url, next)
res.redirect([url | options], next)
A convenience method for 301/302 redirects. Using this method will
tell restify to stop execution of your handler chain. You can also
use an options object. next
is required.
res.redirect('/foo', next);
res.redirect('http://www.foo.com', next);
res.redirect(301, '/foo', next);
res.redirect({
hostname: 'www.foo.com',
pathname: '/bar',
port: 80, // defaults to 80
secure: true, // sets https
permanent: true,
query: {
a: 1
}
}, next); // => redirects to 301 https://www.foo.com/bar?a=1
res.json([status], body)res.json([status], body)
Short-hand for:
res.contentType = 'json';
res.send({hello: 'world'});
Properties
Setting the default headers
You can change what headers restify sends by default by setting the
top-level property restify.defaultResponseHeaders()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
-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-east-1.api.joyent.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-east-1.api.joyent.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
.
restify.createJsonClient(options)createJsonClient(options)
var client = restify.createJsonClient({
url: 'https://api.us-east-1.joyent.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 |
jsonClient.get(path, callback)client.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);
});
jsonClient.head(path, callback)client.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);
});
jsonClient.post(path, object, callback)client.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);
});
jsonClient.put(path, object, callback)client.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);
});
jsonClient.del(path, callback)client.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.
restify.createStringClient(options)restify.createStringClient(options)
var client = restify.createStringClient({
url: 'https://example.com'
})
stringClient.get(path, callback)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);
});
stringClient.head(path, callback)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);
});
stringClient.post(path, object, callback)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);
});
stringClient.put(path, object, callback)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);
});
stringClient.del(path, callback)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).
restify.createClient
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(res.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.
ProxyProxy
There are several options for enabling a proxy for the http client. The following options are available to set a proxy url:
// Set the proxy option in the client configuration
restify.createClient({
proxy: 'http://127.0.0.1'
});
From environment variables:
export HTTPS_PROXY = 'https://127.0.0.1'
$ export HTTP_PROXY = 'http://127.0.0.1'
There is an option to disable the use of a proxy on a url basis or for all urls. This can be enabled by setting an environment variable.
Don't proxy requests to any urls
export NO_PROXY='*'
Don't proxy requests to localhost
export NO_PROXY='127.0.0.1'
Don't proxy requests to localhost on port 8000
export NO_PROXY='localhost:8000'
Don't proxy requests to multiple IPs
export NO_PROXY='127.0.0.1, 8.8.8.8'
Note: The url being requested must match the full hostname in the proxy configuration or NO_PROXY environment variable. DNS lookups are not performed to determine the IP address of a hostname.
client.basicAuth(username, password)client.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) {
res.once('upgradeResult', function(err2, res2, socket2, head2) {
var shed = ws.connect(res2, socket2, head2, wskey);
shed.on('text', function(msg) {
console.log('message from server: ' + msg);
shed.end();
});
shed.send('greetings program');
});
});