Skip to content

Commit 05843fa

Browse files
author
Julien Gilli
committed
refactor to make changes non-breaking
1 parent 612a184 commit 05843fa

8 files changed

+216
-39
lines changed

docs/guides/server.md

+41-14
Original file line numberDiff line numberDiff line change
@@ -425,17 +425,31 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) {
425425
});
426426
```
427427

428-
## Content Negotiation
428+
## Responses' Content Negotiation And Formatting
429429

430430
If you're using `res.send()` restify will determine the content-type to respond
431-
with by:
431+
with by, from highest priority to lowest priority:
432432

433-
1. negotiating the content-type by matching available formatters with the
434-
request's `accept` header
433+
1. using the value of `res.contentType` if present
434+
1. otherwise, using the value of the `Content-Type` response header if set
435435
1. otherwise, using `application/json` if the body is an object that is not a
436436
Buffer instance
437-
1. otherwise, using the value of `res.contentType` if present
438-
1. otherwise, using the value of the `Content-Type` response header if set
437+
1. otherwise, negotiating the content-type by matching available formatters with
438+
the request's `accept` header
439+
440+
If a content-type can't be determined, then restify will respond with an error.
441+
442+
If a content-type can be negotiated, restify then determines what formatter to
443+
use to format the response's content.
444+
445+
If no formatter matching the content-type can be found, restify will by default
446+
override the response's content-type to `'application/octet-stream'` and then
447+
error if no formatter is found for that content-type.
448+
449+
This default behavior can be changed by passing `optionalFormatters: true`
450+
(default is false) when creating the restify server instance. In that case, if
451+
no formatter is found for the negotiated content-type, the response is flushed
452+
without applying any formatter.
439453

440454
Note in the examples above we've not defined any formatters, so we've been
441455
leveraging the fact that restify ships with `application/json`, `text/plain` and
@@ -458,12 +472,6 @@ var server = restify.createServer({
458472
});
459473
```
460474

461-
If a content-type can't be determined, then restify will respond with an error.
462-
463-
If a content-type can be negotiated, but that content-type doesn't have a
464-
corresponding formatter, restify will not format the response with any
465-
formatter.
466-
467475
For example, attempting to send a content-type that does not have a defined
468476
formatter:
469477

@@ -475,8 +483,27 @@ server.get('/foo', function(req, res, next) {
475483
});
476484
```
477485

478-
Will result in a response with a content-type of `text/css` even though no
479-
`'text/css'` formatter is present:
486+
Will result in a response with a content-type of `application/octet-stream`:
487+
488+
```sh
489+
$ curl -i localhost:3000/
490+
HTTP/1.1 200 OK
491+
Content-Type: application/octet-stream
492+
Content-Length: 2
493+
Date: Thu, 02 Jun 2016 06:50:54 GMT
494+
Connection: keep-alive
495+
```
496+
497+
However, if the server instance is created with `optionalFormatters: true`:
498+
499+
```js
500+
var server = restify.createServer({
501+
optionalFormatters: true
502+
});
503+
```
504+
505+
The response has a content-type of `text/css` even though no `'text/css'`
506+
formatter is present:
480507

481508
```sh
482509
$ curl -i localhost:3000/

docs/index.md

+49-14
Original file line numberDiff line numberDiff line change
@@ -435,17 +435,31 @@ server.get('/websocket/attach', function upgradeRoute(req, res, next) {
435435
});
436436
```
437437

438-
## Content Negotiation
438+
## Responses' Content Negotiation And Formatting
439439

440440
If you're using `res.send()` restify will determine the content-type to respond
441-
with by:
441+
with by, from highest priority to lowest priority:
442442

443-
1. negotiating the content-type by matching available formatters with the
444-
request's `accept` header
443+
1. using the value of `res.contentType` if present
444+
1. otherwise, using the value of the `Content-Type` response header if set
445445
1. otherwise, using `application/json` if the body is an object that is not a
446446
Buffer instance
447-
1. otherwise, using the value of `res.contentType` if present
448-
1. otherwise, using the value of the `Content-Type` response header if set
447+
1. otherwise, negotiating the content-type by matching available formatters with
448+
the request's `accept` header
449+
450+
If a content-type can't be determined, then restify will respond with an error.
451+
452+
If a content-type can be negotiated, restify then determines what formatter to
453+
use to format the response's content.
454+
455+
If no formatter matching the content-type can be found, restify will by default
456+
override the response's content-type to `'application/octet-stream'` and then
457+
error if no formatter is found for that content-type.
458+
459+
This default behavior can be changed by passing `optionalFormatters: true`
460+
(default is false) when creating the restify server instance. In that case, if
461+
no formatter is found for the negotiated content-type, the response is flushed
462+
without applying any formatter.
449463

450464
Note in the examples above we've not defined any formatters, so we've been
451465
leveraging the fact that restify ships with `application/json`, `text/plain` and
@@ -468,12 +482,6 @@ var server = restify.createServer({
468482
});
469483
```
470484

471-
If a content-type can't be determined, then restify will respond with an error.
472-
473-
If a content-type can be negotiated, but that content-type doesn't have a
474-
corresponding formatter, restify will not format the response with any
475-
formatter.
476-
477485
For example, attempting to send a content-type that does not have a defined
478486
formatter:
479487

@@ -485,8 +493,27 @@ server.get('/foo', function(req, res, next) {
485493
});
486494
```
487495

488-
Will result in a response with a content-type of `text/css` even though no
489-
`'text/css'` formatter is present:
496+
Will result in a response with a content-type of `application/octet-stream`:
497+
498+
```sh
499+
$ curl -i localhost:3000/
500+
HTTP/1.1 200 OK
501+
Content-Type: application/octet-stream
502+
Content-Length: 2
503+
Date: Thu, 02 Jun 2016 06:50:54 GMT
504+
Connection: keep-alive
505+
```
506+
507+
However, if the server instance is created with `optionalFormatters:true`:
508+
509+
```js
510+
var server = restify.createServer({
511+
optionalFormatters: true
512+
});
513+
```
514+
515+
The response would has a content-type of `text/css` even though no `'text/css'`
516+
formatter is present:
490517

491518
```sh
492519
$ curl -i localhost:3000/
@@ -519,6 +546,14 @@ restify.createServer({
519546
});
520547
```
521548

549+
Restify ships with the following default formatters, which can be overridden
550+
when passing a formatters options to `createServer()`:
551+
552+
* application/javascript
553+
* application/json
554+
* text/plain
555+
* application/octet-stream
556+
522557
The restify response object retains has all the "raw" methods of a node
523558
[ServerResponse](http://nodejs.org/docs/latest/api/http.html#http.ServerResponse)
524559
on it as well.

lib/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ require('./errorTypes');
5959
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
6060
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
6161
* on paths
62+
* @param {Boolean} [options.optionalFormatters=false] - makes finding
63+
* formatters matching responses' content-type optional when sending a response
6264
* @example
6365
* var restify = require('restify');
6466
* var server = restify.createServer();

lib/response.js

+28
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ var HEADER_ARRAY_BLACKLIST = {
3939
* @returns {undefined} No return value
4040
*/
4141
function patch(Response) {
42+
assert.func(Response, 'Response');
43+
4244
/**
4345
* Wraps all of the node
4446
* [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html)
@@ -464,8 +466,33 @@ function patch(Response) {
464466
type = mime.lookup(type);
465467
}
466468

469+
// If we were unable to derive a valid type, default to treating it
470+
// as arbitrary binary data per RFC 2046 Section 4.5.1
471+
if (
472+
!this._optionalFormatters &&
473+
!self.formatters[type] &&
474+
self.acceptable.indexOf(type) === -1
475+
) {
476+
type = 'application/octet-stream';
477+
}
478+
467479
formatter = self.formatters[type] || self.formatters['*/*'];
468480

481+
// If after the above attempts we were still unable to derive a
482+
// formatter, provide a meaningful error message
483+
if (!this._optionalFormatters && !formatter) {
484+
return formatterError(
485+
self,
486+
new errors.InternalServerError({
487+
message:
488+
'could not find formatter for response ' +
489+
'content-type "' +
490+
type +
491+
'"'
492+
})
493+
);
494+
}
495+
469496
if (self._charSet) {
470497
type = type + '; charset=' + self._charSet;
471498
}
@@ -477,6 +504,7 @@ function patch(Response) {
477504
if (formatter) {
478505
// Finally, invoke the formatter and flush the request with it's
479506
// results
507+
480508
return flush(self, formatter(self.req, self, body));
481509
}
482510
}

lib/server.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ var deprecationWarnings = require('./deprecationWarnings');
2626
var patchRequest = require('./request');
2727
var patchResponse = require('./response');
2828

29-
var http2;
30-
3129
patchResponse(http.ServerResponse);
3230
patchRequest(http.IncomingMessage);
3331

32+
var http2;
33+
3434
///--- Globals
3535

3636
var sprintf = util.format;
@@ -82,6 +82,8 @@ var sprintf = util.format;
8282
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
8383
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
8484
* on paths
85+
* @param {Boolean} [options.optionalFormatters=false] - makes finding
86+
* formatters matching responses' content-type optional when sending a response
8587
* @example
8688
* var restify = require('restify');
8789
* var server = restify.createServer();
@@ -103,6 +105,10 @@ function Server(options) {
103105
assert.optionalBool(options.socketio, 'options.socketio');
104106
assert.optionalBool(options.onceNext, 'options.onceNext');
105107
assert.optionalBool(options.strictNext, 'options.strictNext');
108+
assert.optionalBool(
109+
options.optionalFormatters,
110+
'options.optionalFormatters'
111+
);
106112

107113
var self = this;
108114

@@ -126,6 +132,7 @@ function Server(options) {
126132
this.socketio = options.socketio || false;
127133
this.dtrace = options.dtrace || false;
128134
this._inflightRequests = 0;
135+
this.optionalFormatters = options.optionalFormatters || false;
129136

130137
var fmt = mergeFormatters(options.formatters);
131138
this.acceptable = fmt.acceptable;
@@ -1138,6 +1145,7 @@ Server.prototype._setupRequest = function _setupRequest(req, res) {
11381145
res.serverName = self.name;
11391146
res._handlersFinished = false;
11401147
res._flushed = false;
1148+
res._optionalFormatters = this.optionalFormatters;
11411149

11421150
// set header only if name isn't empty string
11431151
if (self.name !== '') {

test/formatter-optional.test.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
/* eslint-disable func-names */
3+
4+
var restifyClients = require('restify-clients');
5+
6+
var restify = require('../lib');
7+
8+
if (require.cache[__dirname + '/lib/helper.js']) {
9+
delete require.cache[__dirname + '/lib/helper.js'];
10+
}
11+
var helper = require('./lib/helper.js');
12+
13+
///--- Globals
14+
15+
var after = helper.after;
16+
var before = helper.before;
17+
var test = helper.test;
18+
19+
var CLIENT;
20+
var LOCALHOST;
21+
var PORT = process.env.UNIT_TEST_PORT || 0;
22+
var SERVER;
23+
24+
///--- Tests
25+
26+
before(function(callback) {
27+
try {
28+
SERVER = restify.createServer({
29+
handleUncaughtExceptions: true,
30+
log: helper.getLog('server'),
31+
optionalFormatters: true
32+
});
33+
SERVER.listen(PORT, '127.0.0.1', function() {
34+
PORT = SERVER.address().port;
35+
CLIENT = restifyClients.createJsonClient({
36+
url: 'http://127.0.0.1:' + PORT,
37+
dtrace: helper.dtrace,
38+
retry: false
39+
});
40+
LOCALHOST = 'http://' + '127.0.0.1:' + PORT;
41+
callback();
42+
});
43+
} catch (e) {
44+
console.error(e.stack);
45+
process.exit(1);
46+
}
47+
});
48+
49+
after(function(callback) {
50+
try {
51+
SERVER.close(callback);
52+
CLIENT.close();
53+
} catch (e) {
54+
console.error(e.stack);
55+
process.exit(1);
56+
}
57+
});
58+
59+
test('should send 200 on formatter missing but optional', function(t) {
60+
// When server is passed "optionalFormatters: true" at creation time,
61+
// res.send still sends a successful response even when a formatter is not
62+
// set up for a specific content-type.
63+
SERVER.get('/11', function handle(req, res, next) {
64+
console.log('got request');
65+
res.header('content-type', 'application/hal+json');
66+
res.send(200, JSON.stringify({ hello: 'world' }));
67+
return next();
68+
});
69+
70+
CLIENT.get(LOCALHOST + '/11', function(err, _, res) {
71+
console.log('got response:', err);
72+
t.ifError(err);
73+
t.equal(res.statusCode, 200);
74+
t.equal(res.headers['content-type'], 'application/hal+json');
75+
t.end();
76+
});
77+
});

test/formatter.test.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ test(
189189
);
190190

191191
test(
192-
'GH-937 should return 200 even when no formatter matching client found ' +
193-
'but content-type set manually',
192+
'GH-937 should return 500 when no default formatter found ' +
193+
'and octet-stream is not available',
194194
function(t) {
195195
// ensure client accepts only a type not specified by server
196196
var opts = {
@@ -201,11 +201,10 @@ test(
201201
};
202202

203203
CLIENT.get(opts, function(err, req, res, data) {
204-
t.ifError(err);
204+
t.ok(err);
205205
t.ok(req);
206206
t.ok(res);
207-
t.equal(res.statusCode, 200);
208-
t.equal(res.contentType(), 'text/html');
207+
t.equal(res.statusCode, 500);
209208
t.end();
210209
});
211210
}

0 commit comments

Comments
 (0)