From 639b48984d6815ba64946f1df7f31c0342738761 Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Fri, 16 Nov 2018 17:17:42 -0800 Subject: [PATCH 1/5] BREAKING CHANGE: do not override content-type in res.send --- docs/guides/server.md | 32 +++++++++++++++++++++++--------- docs/index.md | 32 +++++++++++++++++++++++--------- lib/response.js | 31 ++++++++++--------------------- test/formatter.test.js | 9 +++++---- test/response.test.js | 9 ++++----- 5 files changed, 65 insertions(+), 48 deletions(-) diff --git a/docs/guides/server.md b/docs/guides/server.md index c1a7e5690..64827f655 100644 --- a/docs/guides/server.md +++ b/docs/guides/server.md @@ -427,10 +427,18 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) { ## 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 +If you're using `res.send()` restify will determine the content-type to respond +with by: + +1. negotiating the content-type by matching available formatters with the + request's `accept` header +1. otherwise, using `application/json` if the body is an object that is not a + Buffer instance +1. otherwise, using the value of `res.contentType` if present +1. otherwise, using the value of the `Content-Type` response header if set + +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: @@ -450,9 +458,14 @@ 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: +If a content-type can't be determined, then restify will respond with an error. + +If a content-type can be negotiated, but that content-type doesn't have a +corresponding formatter, restify will not format the response with any +formatter. + +For example, attempting to send a content-type that does not have a defined +formatter: ```js server.get('/foo', function(req, res, next) { @@ -462,12 +475,13 @@ server.get('/foo', function(req, res, next) { }); ``` -Will result in a response with a content-type of `application/octet-stream`: +Will result in a response with 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: application/octet-stream +Content-Type: text/css Content-Length: 2 Date: Thu, 02 Jun 2016 06:50:54 GMT Connection: keep-alive diff --git a/docs/index.md b/docs/index.md index 93c83dab2..66c4e7a56 100644 --- a/docs/index.md +++ b/docs/index.md @@ -437,10 +437,18 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) { ## 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 +If you're using `res.send()` restify will determine the content-type to respond +with by: + +1. negotiating the content-type by matching available formatters with the + request's `accept` header +1. otherwise, using `application/json` if the body is an object that is not a + Buffer instance +1. otherwise, using the value of `res.contentType` if present +1. otherwise, using the value of the `Content-Type` response header if set + +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: @@ -460,9 +468,14 @@ 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: +If a content-type can't be determined, then restify will respond with an error. + +If a content-type can be negotiated, but that content-type doesn't have a +corresponding formatter, restify will not format the response with any +formatter. + +For example, attempting to send a content-type that does not have a defined +formatter: ```js server.get('/foo', function(req, res, next) { @@ -472,12 +485,13 @@ server.get('/foo', function(req, res, next) { }); ``` -Will result in a response with a content-type of `application/octet-stream`: +Will result in a response with 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: application/octet-stream +Content-Type: text/css Content-Length: 2 Date: Thu, 02 Jun 2016 06:50:54 GMT Connection: keep-alive diff --git a/lib/response.js b/lib/response.js index 96d22bf47..80382fa1a 100644 --- a/lib/response.js +++ b/lib/response.js @@ -448,12 +448,14 @@ function patch(Response) { // Derive type if not provided by the user type = type || self.req.accepts(self.acceptable); - // Check to see if we can find a valid formatter + // 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 formatter' + message: + 'could not find suitable content-type to use ' + + 'for the response' }) ); } @@ -464,33 +466,20 @@ function patch(Response) { type = mime.lookup(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'; - } - 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' - }) - ); - } - if (self._charSet) { type = type + '; charset=' + self._charSet; } - // Update header to the derived content type for our formatter + // 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 (!formatter) { + return flush(self, body); + } + // Finally, invoke the formatter and flush the request with it's results return flush(self, formatter(self.req, self, body)); }; diff --git a/test/formatter.test.js b/test/formatter.test.js index b1ff61cef..28d1730ce 100644 --- a/test/formatter.test.js +++ b/test/formatter.test.js @@ -189,8 +189,8 @@ test( ); test( - 'GH-937 should return 500 when no default formatter found ' + - 'and octet-stream is not available', + 'GH-937 should return 200 even when no formatter matching client found ' + + 'but content-type set manually', function(t) { // ensure client accepts only a type not specified by server var opts = { @@ -201,10 +201,11 @@ test( }; CLIENT.get(opts, function(err, req, res, data) { - t.ok(err); + t.ifError(err); t.ok(req); t.ok(res); - t.equal(res.statusCode, 500); + t.equal(res.statusCode, 200); + t.equal(res.contentType(), 'text/html'); t.end(); }); } diff --git a/test/response.test.js b/test/response.test.js index a3866af21..8605c7551 100644 --- a/test/response.test.js +++ b/test/response.test.js @@ -497,10 +497,9 @@ test('writeHead should emit a header event', function(t) { }); }); -test('should fail to set header due to missing formatter', function(t) { - // when a formatter is not set up for a specific content-type, restify will - // default to octet-stream. - +test('should send 200 when formatter missing', function(t) { + // res.send still sends a response even when a formatter is not set up for a + // specific content-type. SERVER.get('/11', function handle(req, res, next) { res.header('content-type', 'application/hal+json'); res.send(200, JSON.stringify({ hello: 'world' })); @@ -510,7 +509,7 @@ test('should fail to set header due to missing formatter', function(t) { CLIENT.get(join(LOCALHOST, '/11'), function(err, _, res) { t.ifError(err); t.equal(res.statusCode, 200); - t.equal(res.headers['content-type'], 'application/octet-stream'); + t.equal(res.headers['content-type'], 'application/hal+json'); t.end(); }); }); From 612a18430eb54a084c300ea53b8ca108fb7de11e Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Thu, 29 Nov 2018 17:36:00 -0800 Subject: [PATCH 2/5] assert on unformatted body including in the res.send case when no formatter found --- lib/response.js | 120 ++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/lib/response.js b/lib/response.js index 80382fa1a..f17bbe544 100644 --- a/lib/response.js +++ b/lib/response.js @@ -346,6 +346,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); }; @@ -417,71 +422,66 @@ 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 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 (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); + } - // 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); + + // 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' + }) + ); + } - type = type.split(';')[0]; + type = type.split(';')[0]; - if (!self.formatters[type] && type.indexOf('/') === -1) { - type = mime.lookup(type); - } + if (!self.formatters[type] && type.indexOf('/') === -1) { + type = mime.lookup(type); + } - formatter = self.formatters[type] || self.formatters['*/*']; + formatter = self.formatters[type] || self.formatters['*/*']; - if (self._charSet) { - type = type + '; charset=' + self._charSet; - } + if (self._charSet) { + type = type + '; charset=' + self._charSet; + } - // 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); + // 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 (!formatter) { - return flush(self, body); + if (formatter) { + // Finally, invoke the formatter and flush the request with it's + // results + 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); }; /** @@ -802,11 +802,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); From 20566cf3547dc46724203d463d4b548fabb5632d Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Tue, 4 Dec 2018 17:15:17 -0800 Subject: [PATCH 3/5] refactor to make changes non-breaking --- docs/guides/server.md | 55 +++++++++++++++++------ docs/index.md | 63 +++++++++++++++++++++------ lib/index.js | 2 + lib/response.js | 29 +++++++++++++ lib/server.js | 8 ++++ test/formatter-optional.test.js | 77 +++++++++++++++++++++++++++++++++ test/formatter.test.js | 9 ++-- test/response.test.js | 9 ++-- 8 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 test/formatter-optional.test.js diff --git a/docs/guides/server.md b/docs/guides/server.md index 64827f655..43cb98412 100644 --- a/docs/guides/server.md +++ b/docs/guides/server.md @@ -425,17 +425,31 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) { }); ``` -## Content Negotiation +## Responses' Content Negotiation And Formatting If you're using `res.send()` restify will determine the content-type to respond -with by: +with by, from highest priority to lowest priority: -1. negotiating the content-type by matching available formatters with the - request's `accept` header +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, using the value of `res.contentType` if present -1. otherwise, using the value of the `Content-Type` response header if set +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 `optionalFormatters: true` +(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 @@ -458,12 +472,6 @@ var server = restify.createServer({ }); ``` -If a content-type can't be determined, then restify will respond with an error. - -If a content-type can be negotiated, but that content-type doesn't have a -corresponding formatter, restify will not format the response with any -formatter. - For example, attempting to send a content-type that does not have a defined formatter: @@ -475,8 +483,27 @@ server.get('/foo', function(req, res, next) { }); ``` -Will result in a response with a content-type of `text/css` even though no -`'text/css'` formatter is present: +Will result in a response with a content-type of `application/octet-stream`: + +```sh +$ curl -i localhost:3000/ +HTTP/1.1 200 OK +Content-Type: application/octet-stream +Content-Length: 2 +Date: Thu, 02 Jun 2016 06:50:54 GMT +Connection: keep-alive +``` + +However, if the server instance is created with `optionalFormatters: true`: + +```js +var server = restify.createServer({ + optionalFormatters: true +}); +``` + +The response has a content-type of `text/css` even though no `'text/css'` +formatter is present: ```sh $ curl -i localhost:3000/ diff --git a/docs/index.md b/docs/index.md index 66c4e7a56..b0c8f873c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -435,17 +435,31 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) { }); ``` -## Content Negotiation +## Responses' Content Negotiation And Formatting If you're using `res.send()` restify will determine the content-type to respond -with by: +with by, from highest priority to lowest priority: -1. negotiating the content-type by matching available formatters with the - request's `accept` header +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, using the value of `res.contentType` if present -1. otherwise, using the value of the `Content-Type` response header if set +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 `optionalFormatters: true` +(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 @@ -468,12 +482,6 @@ var server = restify.createServer({ }); ``` -If a content-type can't be determined, then restify will respond with an error. - -If a content-type can be negotiated, but that content-type doesn't have a -corresponding formatter, restify will not format the response with any -formatter. - For example, attempting to send a content-type that does not have a defined formatter: @@ -485,8 +493,27 @@ server.get('/foo', function(req, res, next) { }); ``` -Will result in a response with a content-type of `text/css` even though no -`'text/css'` formatter is present: +Will result in a response with a content-type of `application/octet-stream`: + +```sh +$ curl -i localhost:3000/ +HTTP/1.1 200 OK +Content-Type: application/octet-stream +Content-Length: 2 +Date: Thu, 02 Jun 2016 06:50:54 GMT +Connection: keep-alive +``` + +However, if the server instance is created with `optionalFormatters:true`: + +```js +var server = restify.createServer({ + optionalFormatters: true +}); +``` + +The response would has a content-type of `text/css` even though no `'text/css'` +formatter is present: ```sh $ curl -i localhost:3000/ @@ -519,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. diff --git a/lib/index.js b/lib/index.js index 76e35fa5c..b4e5f3cf7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -59,6 +59,8 @@ require('./errorTypes'); * `res.writeContinue()` in `server.on('checkContinue')` when proxing * @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash * on paths + * @param {Boolean} [options.optionalFormatters=false] - makes finding + * formatters matching responses' content-type optional when sending a response * @example * var restify = require('restify'); * var server = restify.createServer(); diff --git a/lib/response.js b/lib/response.js index f17bbe544..271dc7071 100644 --- a/lib/response.js +++ b/lib/response.js @@ -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) @@ -464,8 +466,34 @@ function patch(Response) { type = mime.lookup(type); } + // 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._optionalFormatters && + !self.formatters[type] && + self.acceptable.indexOf(type) === -1 + ) { + type = 'application/octet-stream'; + } + 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._optionalFormatters && !formatter) { + return formatterError( + self, + new errors.InternalServerError({ + message: + 'could not find formatter for response ' + + 'content-type "' + + type + + '"' + }) + ); + } + if (self._charSet) { type = type + '; charset=' + self._charSet; } @@ -477,6 +505,7 @@ function patch(Response) { if (formatter) { // Finally, invoke the formatter and flush the request with it's // results + return flush(self, formatter(self.req, self, body)); } } diff --git a/lib/server.js b/lib/server.js index f47deb446..d7eae8eea 100644 --- a/lib/server.js +++ b/lib/server.js @@ -82,6 +82,8 @@ var sprintf = util.format; * `res.writeContinue()` in `server.on('checkContinue')` when proxing * @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash * on paths + * @param {Boolean} [options.optionalFormatters=false] - makes finding + * formatters matching responses' content-type optional when sending a response * @example * var restify = require('restify'); * var server = restify.createServer(); @@ -103,6 +105,10 @@ function Server(options) { assert.optionalBool(options.socketio, 'options.socketio'); assert.optionalBool(options.onceNext, 'options.onceNext'); assert.optionalBool(options.strictNext, 'options.strictNext'); + assert.optionalBool( + options.optionalFormatters, + 'options.optionalFormatters' + ); var self = this; @@ -126,6 +132,7 @@ function Server(options) { this.socketio = options.socketio || false; this.dtrace = options.dtrace || false; this._inflightRequests = 0; + this.optionalFormatters = options.optionalFormatters || false; var fmt = mergeFormatters(options.formatters); this.acceptable = fmt.acceptable; @@ -1138,6 +1145,7 @@ Server.prototype._setupRequest = function _setupRequest(req, res) { res.serverName = self.name; res._handlersFinished = false; res._flushed = false; + res._optionalFormatters = this.optionalFormatters; // set header only if name isn't empty string if (self.name !== '') { diff --git a/test/formatter-optional.test.js b/test/formatter-optional.test.js new file mode 100644 index 000000000..07da952a1 --- /dev/null +++ b/test/formatter-optional.test.js @@ -0,0 +1,77 @@ +'use strict'; +/* eslint-disable func-names */ + +var restifyClients = require('restify-clients'); + +var restify = require('../lib'); + +if (require.cache[__dirname + '/lib/helper.js']) { + delete require.cache[__dirname + '/lib/helper.js']; +} +var helper = require('./lib/helper.js'); + +///--- Globals + +var after = helper.after; +var before = helper.before; +var test = helper.test; + +var CLIENT; +var LOCALHOST; +var PORT = process.env.UNIT_TEST_PORT || 0; +var SERVER; + +///--- Tests + +before(function(callback) { + try { + SERVER = restify.createServer({ + handleUncaughtExceptions: true, + log: helper.getLog('server'), + optionalFormatters: true + }); + SERVER.listen(PORT, '127.0.0.1', function() { + PORT = SERVER.address().port; + CLIENT = restifyClients.createJsonClient({ + url: 'http://127.0.0.1:' + PORT, + dtrace: helper.dtrace, + retry: false + }); + LOCALHOST = 'http://' + '127.0.0.1:' + PORT; + callback(); + }); + } catch (e) { + console.error(e.stack); + process.exit(1); + } +}); + +after(function(callback) { + try { + SERVER.close(callback); + CLIENT.close(); + } catch (e) { + console.error(e.stack); + process.exit(1); + } +}); + +test('should send 200 on formatter missing but optional', function(t) { + // When server is passed "optionalFormatters: true" at creation time, + // res.send still sends a successful response even when a formatter is not + // set up for a specific content-type. + SERVER.get('/11', function handle(req, res, next) { + console.log('got request'); + res.header('content-type', 'application/hal+json'); + res.send(200, JSON.stringify({ hello: 'world' })); + return next(); + }); + + CLIENT.get(LOCALHOST + '/11', function(err, _, res) { + console.log('got response:', err); + t.ifError(err); + t.equal(res.statusCode, 200); + t.equal(res.headers['content-type'], 'application/hal+json'); + t.end(); + }); +}); diff --git a/test/formatter.test.js b/test/formatter.test.js index 28d1730ce..b1ff61cef 100644 --- a/test/formatter.test.js +++ b/test/formatter.test.js @@ -189,8 +189,8 @@ test( ); test( - 'GH-937 should return 200 even when no formatter matching client found ' + - 'but content-type set manually', + 'GH-937 should return 500 when no default formatter found ' + + 'and octet-stream is not available', function(t) { // ensure client accepts only a type not specified by server var opts = { @@ -201,11 +201,10 @@ test( }; CLIENT.get(opts, function(err, req, res, data) { - t.ifError(err); + t.ok(err); t.ok(req); t.ok(res); - t.equal(res.statusCode, 200); - t.equal(res.contentType(), 'text/html'); + t.equal(res.statusCode, 500); t.end(); }); } diff --git a/test/response.test.js b/test/response.test.js index 8605c7551..a3866af21 100644 --- a/test/response.test.js +++ b/test/response.test.js @@ -497,9 +497,10 @@ test('writeHead should emit a header event', function(t) { }); }); -test('should send 200 when formatter missing', function(t) { - // res.send still sends a response even when a formatter is not set up for a - // specific content-type. +test('should fail to set header due to missing formatter', function(t) { + // when a formatter is not set up for a specific content-type, restify will + // default to octet-stream. + SERVER.get('/11', function handle(req, res, next) { res.header('content-type', 'application/hal+json'); res.send(200, JSON.stringify({ hello: 'world' })); @@ -509,7 +510,7 @@ test('should send 200 when formatter missing', function(t) { CLIENT.get(join(LOCALHOST, '/11'), function(err, _, res) { t.ifError(err); t.equal(res.statusCode, 200); - t.equal(res.headers['content-type'], 'application/hal+json'); + t.equal(res.headers['content-type'], 'application/octet-stream'); t.end(); }); }); From 17a12cd59f3bf0ce5ce58603bad0280b7a66f5cd Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Thu, 6 Dec 2018 13:15:10 -0800 Subject: [PATCH 4/5] remove leftover debug logs --- test/formatter-optional.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/formatter-optional.test.js b/test/formatter-optional.test.js index 07da952a1..8e50970d3 100644 --- a/test/formatter-optional.test.js +++ b/test/formatter-optional.test.js @@ -61,14 +61,12 @@ test('should send 200 on formatter missing but optional', function(t) { // res.send still sends a successful response even when a formatter is not // set up for a specific content-type. SERVER.get('/11', function handle(req, res, next) { - console.log('got request'); res.header('content-type', 'application/hal+json'); res.send(200, JSON.stringify({ hello: 'world' })); return next(); }); CLIENT.get(LOCALHOST + '/11', function(err, _, res) { - console.log('got response:', err); t.ifError(err); t.equal(res.statusCode, 200); t.equal(res.headers['content-type'], 'application/hal+json'); From df57d728c1f339197ba928f2ab4d28ebf5488963 Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Thu, 6 Dec 2018 13:33:50 -0800 Subject: [PATCH 5/5] optionalFormatters -> strictFormatters --- docs/guides/server.md | 10 +++++----- docs/index.md | 6 +++--- lib/index.js | 7 +++++-- lib/response.js | 4 ++-- lib/server.js | 20 ++++++++++++-------- test/formatter-optional.test.js | 10 +++++----- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/docs/guides/server.md b/docs/guides/server.md index 43cb98412..e90ecf610 100644 --- a/docs/guides/server.md +++ b/docs/guides/server.md @@ -446,9 +446,9 @@ 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 `optionalFormatters: true` -(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 +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 @@ -494,11 +494,11 @@ Date: Thu, 02 Jun 2016 06:50:54 GMT Connection: keep-alive ``` -However, if the server instance is created with `optionalFormatters: true`: +However, if the server instance is created with `strictFormatters: false`: ```js var server = restify.createServer({ - optionalFormatters: true + strictFormatters: false }); ``` diff --git a/docs/index.md b/docs/index.md index b0c8f873c..f6d17e2e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -456,7 +456,7 @@ 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 `optionalFormatters: true` +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. @@ -504,11 +504,11 @@ Date: Thu, 02 Jun 2016 06:50:54 GMT Connection: keep-alive ``` -However, if the server instance is created with `optionalFormatters:true`: +However, if the server instance is created with `strictFormatters:false`: ```js var server = restify.createServer({ - optionalFormatters: true + strictFormatters: false }); ``` diff --git a/lib/index.js b/lib/index.js index b4e5f3cf7..6cc29fd29 100644 --- a/lib/index.js +++ b/lib/index.js @@ -59,8 +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.optionalFormatters=false] - makes finding - * formatters matching responses' content-type optional when sending a response + * @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(); diff --git a/lib/response.js b/lib/response.js index 271dc7071..a7f817546 100644 --- a/lib/response.js +++ b/lib/response.js @@ -470,7 +470,7 @@ function patch(Response) { // 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._optionalFormatters && + this._strictFormatters && !self.formatters[type] && self.acceptable.indexOf(type) === -1 ) { @@ -481,7 +481,7 @@ function patch(Response) { // If after the above attempts we were still unable to derive a // formatter, provide a meaningful error message - if (!this._optionalFormatters && !formatter) { + if (this._strictFormatters && !formatter) { return formatterError( self, new errors.InternalServerError({ diff --git a/lib/server.js b/lib/server.js index d7eae8eea..0b81f4424 100644 --- a/lib/server.js +++ b/lib/server.js @@ -82,8 +82,11 @@ var sprintf = util.format; * `res.writeContinue()` in `server.on('checkContinue')` when proxing * @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash * on paths - * @param {Boolean} [options.optionalFormatters=false] - makes finding - * formatters matching responses' content-type optional when sending a response + * @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(); @@ -105,10 +108,7 @@ function Server(options) { assert.optionalBool(options.socketio, 'options.socketio'); assert.optionalBool(options.onceNext, 'options.onceNext'); assert.optionalBool(options.strictNext, 'options.strictNext'); - assert.optionalBool( - options.optionalFormatters, - 'options.optionalFormatters' - ); + assert.optionalBool(options.strictFormatters, 'options.strictFormatters'); var self = this; @@ -132,7 +132,11 @@ function Server(options) { this.socketio = options.socketio || false; this.dtrace = options.dtrace || false; this._inflightRequests = 0; - this.optionalFormatters = options.optionalFormatters || false; + + this.strictFormatters = true; + if (options.strictFormatters !== undefined) { + this.strictFormatters = options.strictFormatters; + } var fmt = mergeFormatters(options.formatters); this.acceptable = fmt.acceptable; @@ -1145,7 +1149,7 @@ Server.prototype._setupRequest = function _setupRequest(req, res) { res.serverName = self.name; res._handlersFinished = false; res._flushed = false; - res._optionalFormatters = this.optionalFormatters; + res._strictFormatters = this.strictFormatters; // set header only if name isn't empty string if (self.name !== '') { diff --git a/test/formatter-optional.test.js b/test/formatter-optional.test.js index 8e50970d3..9ea729837 100644 --- a/test/formatter-optional.test.js +++ b/test/formatter-optional.test.js @@ -28,7 +28,7 @@ before(function(callback) { SERVER = restify.createServer({ handleUncaughtExceptions: true, log: helper.getLog('server'), - optionalFormatters: true + strictFormatters: false }); SERVER.listen(PORT, '127.0.0.1', function() { PORT = SERVER.address().port; @@ -56,10 +56,10 @@ after(function(callback) { } }); -test('should send 200 on formatter missing but optional', function(t) { - // When server is passed "optionalFormatters: true" at creation time, - // res.send still sends a successful response even when a formatter is not - // set up for a specific content-type. +test('send 200 on formatter missing and strictFormatters false', function(t) { + // When server is passed "strictFormatters: false" at creation time, + // res.send still sends a successful response even when a formatter is + // not set up for a specific content-type. SERVER.get('/11', function handle(req, res, next) { res.header('content-type', 'application/hal+json'); res.send(200, JSON.stringify({ hello: 'world' }));