Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: add support for non-strict formatters (#1721)
  • Loading branch information
misterdjules committed Dec 7, 2018
1 parent 1bc2dda commit de1833a
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 82 deletions.
57 changes: 49 additions & 8 deletions docs/guides/server.md
Expand Up @@ -425,12 +425,34 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) {
});
```

## Content Negotiation
## Responses' Content Negotiation And Formatting

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
If you're using `res.send()` restify will determine the content-type to respond
with by, from highest priority to lowest priority:

1. using the value of `res.contentType` if present
1. otherwise, using the value of the `Content-Type` response header if set
1. otherwise, using `application/json` if the body is an object that is not a
Buffer instance
1. otherwise, negotiating the content-type by matching available formatters with
the request's `accept` header

If a content-type can't be determined, then restify will respond with an error.

If a content-type can be negotiated, restify then determines what formatter to
use to format the response's content.

If no formatter matching the content-type can be found, restify will by default
override the response's content-type to `'application/octet-stream'` and then
error if no formatter is found for that content-type.

This default behavior can be changed by passing `strictFormatters: false`
(default is true) when creating the restify server instance. In that case, if no
formatter is found for the negotiated content-type, the response is flushed
without applying any formatter.

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:

Expand All @@ -450,9 +472,8 @@ var server = restify.createServer({
});
```

If a content-type can't be negotiated, then restify will default to using the
`application/octet-stream` formatter. For example, attempting to send a
content-type that does not have a defined formatter:
For example, attempting to send a content-type that does not have a defined
formatter:

```js
server.get('/foo', function(req, res, next) {
Expand All @@ -473,6 +494,26 @@ Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```

However, if the server instance is created with `strictFormatters: false`:

```js
var server = restify.createServer({
strictFormatters: false
});
```

The response has a content-type of `text/css` even though no `'text/css'`
formatter is present:

```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```

As previously noted, restify ships with built-in formatters for 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`
Expand Down
65 changes: 57 additions & 8 deletions docs/index.md
Expand Up @@ -435,12 +435,34 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) {
});
```

## Content Negotiation
## Responses' Content Negotiation And Formatting

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
If you're using `res.send()` restify will determine the content-type to respond
with by, from highest priority to lowest priority:

1. using the value of `res.contentType` if present
1. otherwise, using the value of the `Content-Type` response header if set
1. otherwise, using `application/json` if the body is an object that is not a
Buffer instance
1. otherwise, negotiating the content-type by matching available formatters with
the request's `accept` header

If a content-type can't be determined, then restify will respond with an error.

If a content-type can be negotiated, restify then determines what formatter to
use to format the response's content.

If no formatter matching the content-type can be found, restify will by default
override the response's content-type to `'application/octet-stream'` and then
error if no formatter is found for that content-type.

This default behavior can be changed by passing `strictFormatters: false`
(default is false) when creating the restify server instance. In that case, if
no formatter is found for the negotiated content-type, the response is flushed
without applying any formatter.

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:

Expand All @@ -460,9 +482,8 @@ var server = restify.createServer({
});
```

If a content-type can't be negotiated, then restify will default to using the
`application/octet-stream` formatter. For example, attempting to send a
content-type that does not have a defined formatter:
For example, attempting to send a content-type that does not have a defined
formatter:

```js
server.get('/foo', function(req, res, next) {
Expand All @@ -483,6 +504,26 @@ Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```

However, if the server instance is created with `strictFormatters:false`:

```js
var server = restify.createServer({
strictFormatters: false
});
```

The response would has a content-type of `text/css` even though no `'text/css'`
formatter is present:

```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```

As previously noted, restify ships with built-in formatters for 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`
Expand All @@ -505,6 +546,14 @@ restify.createServer({
});
```

Restify ships with the following default formatters, which can be overridden
when passing a formatters options to `createServer()`:

* application/javascript
* application/json
* text/plain
* application/octet-stream

The restify response object retains has all the "raw" methods of a node
[ServerResponse](http://nodejs.org/docs/latest/api/http.html#http.ServerResponse)
on it as well.
Expand Down
5 changes: 5 additions & 0 deletions lib/index.js
Expand Up @@ -59,6 +59,11 @@ require('./errorTypes');
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
* on paths
* @param {Boolean} [options.strictFormatters=true] - enables strict formatters
* behavior: a formatter matching the response's content-type is required. If
* not found, the response's content-type is automatically set to
* 'application/octet-stream'. If a formatter for that content-type is not
* found, sending the response errors.
* @example
* var restify = require('restify');
* var server = restify.createServer();
Expand Down
158 changes: 92 additions & 66 deletions lib/response.js
Expand Up @@ -39,6 +39,8 @@ var HEADER_ARRAY_BLACKLIST = {
* @returns {undefined} No return value
*/
function patch(Response) {
assert.func(Response, 'Response');

/**
* Wraps all of the node
* [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html)
Expand Down Expand Up @@ -346,6 +348,11 @@ function patch(Response) {
};
}

assert.ok(
typeof sendArgs.body === 'string' || Buffer.isBuffer(sendArgs.body),
'res.sendRaw() accepts only strings or buffers'
);

sendArgs.format = false;
return self.__send(sendArgs);
};
Expand Down Expand Up @@ -417,82 +424,93 @@ function patch(Response) {
return flush(self);
}

// if no formatting, assert that the value to be written is a string
// or a buffer, then send it.
if (opts.format === false) {
assert.ok(
typeof body === 'string' || Buffer.isBuffer(body),
'res.sendRaw() accepts only strings or buffers'
);
return flush(self, body);
}
if (opts.format === true) {
// if no body, then no need to format. if this was an error caught
// by a domain, don't send the domain error either.
if (body === undefined || (body instanceof Error && body.domain)) {
return flush(self);
}

// if no body, then no need to format. if this was an error caught by a
// domain, don't send the domain error either.
if (body === undefined || (body instanceof Error && body.domain)) {
return flush(self);
}
// At this point we know we have a body that needs to be formatted,
// so lets derive the formatter based on the response object's
// properties

// At this point we know we have a body that needs to be formatted, so
// lets derive the formatter based on the response object's properties
var formatter;
var type = self.contentType || self.getHeader('Content-Type');

var formatter;
var type = self.contentType || self.getHeader('Content-Type');
// Set Content-Type to application/json when
// res.send is called with an Object instead of calling res.json
if (!type && typeof body === 'object' && !Buffer.isBuffer(body)) {
type = 'application/json';
}

// Set Content-Type to application/json when
// res.send is called with an Object instead of calling res.json
if (!type && typeof body === 'object' && !Buffer.isBuffer(body)) {
type = 'application/json';
}
// Derive type if not provided by the user
type = type || self.req.accepts(self.acceptable);

// Check to see if we could find a content type to use for the
// response.
if (!type) {
return formatterError(
self,
new errors.NotAcceptableError({
message:
'could not find suitable content-type to use ' +
'for the response'
})
);
}

// Derive type if not provided by the user
type = type || self.req.accepts(self.acceptable);
type = type.split(';')[0];

// Check to see if we can find a valid formatter
if (!type) {
return formatterError(
self,
new errors.NotAcceptableError({
message: 'could not find suitable formatter'
})
);
}
if (!self.formatters[type] && type.indexOf('/') === -1) {
type = mime.lookup(type);
}

type = type.split(';')[0];
// If finding a formatter matching the negotiated content-type is
// required, and we were unable to derive a valid type, default to
// treating it as arbitrary binary data per RFC 2046 Section 4.5.1
if (
this._strictFormatters &&
!self.formatters[type] &&
self.acceptable.indexOf(type) === -1
) {
type = 'application/octet-stream';
}

if (!self.formatters[type] && type.indexOf('/') === -1) {
type = mime.lookup(type);
}
formatter = self.formatters[type] || self.formatters['*/*'];

// If after the above attempts we were still unable to derive a
// formatter, provide a meaningful error message
if (this._strictFormatters && !formatter) {
return formatterError(
self,
new errors.InternalServerError({
message:
'could not find formatter for response ' +
'content-type "' +
type +
'"'
})
);
}

// If we were unable to derive a valid type, default to treating it as
// arbitrary binary data per RFC 2046 Section 4.5.1
if (!self.formatters[type] && self.acceptable.indexOf(type) === -1) {
type = 'application/octet-stream';
}
if (self._charSet) {
type = type + '; charset=' + self._charSet;
}

formatter = self.formatters[type] || self.formatters['*/*'];

// If after the above attempts we were still unable to derive a
// formatter, provide a meaningful error message
if (!formatter) {
return formatterError(
self,
new errors.InternalServerError({
message:
'could not find formatter for application/octet-stream'
})
);
}
// Update Content-Type header to the one originally set or to the
// type inferred from the most relevant formatter found.
self.setHeader('Content-Type', type);

if (self._charSet) {
type = type + '; charset=' + self._charSet;
}
if (formatter) {
// Finally, invoke the formatter and flush the request with it's
// results

// Update header to the derived content type for our formatter
self.setHeader('Content-Type', type);
return flush(self, formatter(self.req, self, body));
}
}

// Finally, invoke the formatter and flush the request with it's results
return flush(self, formatter(self.req, self, body));
return flush(self, body);
};

/**
Expand Down Expand Up @@ -813,11 +831,19 @@ function patch(Response) {
* @private
* @function flush
* @param {Response} res - response
* @param {String|Buffer} formattedBody - formatted body
* @param {String|Buffer} body - response body
* @returns {Response} response
*/
function flush(res, formattedBody) {
res._data = formattedBody;
function flush(res, body) {
assert.ok(
body === null ||
body === undefined ||
typeof body === 'string' ||
Buffer.isBuffer(body),
'body must be a string or a Buffer instance'
);

res._data = body;

// Flush headers
res.writeHead(res.statusCode);
Expand Down

0 comments on commit de1833a

Please sign in to comment.