Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for non-strict formatters #1721

Merged
merged 5 commits into from Dec 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

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