5.0.0
[express.git] / lib / response.js
blob937e9858535c29e687f5c6b476174cade617ba47
1 /*!
2  * express
3  * Copyright(c) 2009-2013 TJ Holowaychuk
4  * Copyright(c) 2014-2015 Douglas Christopher Wilson
5  * MIT Licensed
6  */
8 'use strict';
10 /**
11  * Module dependencies.
12  * @private
13  */
15 var Buffer = require('safe-buffer').Buffer
16 var contentDisposition = require('content-disposition');
17 var createError = require('http-errors')
18 var encodeUrl = require('encodeurl');
19 var escapeHtml = require('escape-html');
20 var http = require('http');
21 var onFinished = require('on-finished');
22 var mime = require('mime-types')
23 var path = require('path');
24 var pathIsAbsolute = require('path').isAbsolute;
25 var statuses = require('statuses')
26 var merge = require('utils-merge');
27 var sign = require('cookie-signature').sign;
28 var normalizeType = require('./utils').normalizeType;
29 var normalizeTypes = require('./utils').normalizeTypes;
30 var setCharset = require('./utils').setCharset;
31 var cookie = require('cookie');
32 var send = require('send');
33 var extname = path.extname;
34 var resolve = path.resolve;
35 var vary = require('vary');
37 /**
38  * Response prototype.
39  * @public
40  */
42 var res = Object.create(http.ServerResponse.prototype)
44 /**
45  * Module exports.
46  * @public
47  */
49 module.exports = res
51 /**
52  * Set the HTTP status code for the response.
53  *
54  * Expects an integer value between 100 and 999 inclusive.
55  * Throws an error if the provided status code is not an integer or if it's outside the allowable range.
56  *
57  * @param {number} code - The HTTP status code to set.
58  * @return {ServerResponse} - Returns itself for chaining methods.
59  * @throws {TypeError} If `code` is not an integer.
60  * @throws {RangeError} If `code` is outside the range 100 to 999.
61  * @public
62  */
64 res.status = function status(code) {
65   // Check if the status code is not an integer
66   if (!Number.isInteger(code)) {
67     throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);
68   }
69   // Check if the status code is outside of Node's valid range
70   if (code < 100 || code > 999) {
71     throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`);
72   }
74   this.statusCode = code;
75   return this;
78 /**
79  * Set Link header field with the given `links`.
80  *
81  * Examples:
82  *
83  *    res.links({
84  *      next: 'http://api.example.com/users?page=2',
85  *      last: 'http://api.example.com/users?page=5'
86  *    });
87  *
88  * @param {Object} links
89  * @return {ServerResponse}
90  * @public
91  */
93 res.links = function(links){
94   var link = this.get('Link') || '';
95   if (link) link += ', ';
96   return this.set('Link', link + Object.keys(links).map(function(rel){
97     return '<' + links[rel] + '>; rel="' + rel + '"';
98   }).join(', '));
102  * Send a response.
104  * Examples:
106  *     res.send(Buffer.from('wahoo'));
107  *     res.send({ some: 'json' });
108  *     res.send('<p>some html</p>');
110  * @param {string|number|boolean|object|Buffer} body
111  * @public
112  */
114 res.send = function send(body) {
115   var chunk = body;
116   var encoding;
117   var req = this.req;
118   var type;
120   // settings
121   var app = this.app;
123   switch (typeof chunk) {
124     // string defaulting to html
125     case 'string':
126       if (!this.get('Content-Type')) {
127         this.type('html');
128       }
129       break;
130     case 'boolean':
131     case 'number':
132     case 'object':
133       if (chunk === null) {
134         chunk = '';
135       } else if (Buffer.isBuffer(chunk)) {
136         if (!this.get('Content-Type')) {
137           this.type('bin');
138         }
139       } else {
140         return this.json(chunk);
141       }
142       break;
143   }
145   // write strings in utf-8
146   if (typeof chunk === 'string') {
147     encoding = 'utf8';
148     type = this.get('Content-Type');
150     // reflect this in content-type
151     if (typeof type === 'string') {
152       this.set('Content-Type', setCharset(type, 'utf-8'));
153     }
154   }
156   // determine if ETag should be generated
157   var etagFn = app.get('etag fn')
158   var generateETag = !this.get('ETag') && typeof etagFn === 'function'
160   // populate Content-Length
161   var len
162   if (chunk !== undefined) {
163     if (Buffer.isBuffer(chunk)) {
164       // get length of Buffer
165       len = chunk.length
166     } else if (!generateETag && chunk.length < 1000) {
167       // just calculate length when no ETag + small chunk
168       len = Buffer.byteLength(chunk, encoding)
169     } else {
170       // convert chunk to Buffer and calculate
171       chunk = Buffer.from(chunk, encoding)
172       encoding = undefined;
173       len = chunk.length
174     }
176     this.set('Content-Length', len);
177   }
179   // populate ETag
180   var etag;
181   if (generateETag && len !== undefined) {
182     if ((etag = etagFn(chunk, encoding))) {
183       this.set('ETag', etag);
184     }
185   }
187   // freshness
188   if (req.fresh) this.status(304);
190   // strip irrelevant headers
191   if (204 === this.statusCode || 304 === this.statusCode) {
192     this.removeHeader('Content-Type');
193     this.removeHeader('Content-Length');
194     this.removeHeader('Transfer-Encoding');
195     chunk = '';
196   }
198   // alter headers for 205
199   if (this.statusCode === 205) {
200     this.set('Content-Length', '0')
201     this.removeHeader('Transfer-Encoding')
202     chunk = ''
203   }
205   if (req.method === 'HEAD') {
206     // skip body for HEAD
207     this.end();
208   } else {
209     // respond
210     this.end(chunk, encoding);
211   }
213   return this;
217  * Send JSON response.
219  * Examples:
221  *     res.json(null);
222  *     res.json({ user: 'tj' });
224  * @param {string|number|boolean|object} obj
225  * @public
226  */
228 res.json = function json(obj) {
229   // settings
230   var app = this.app;
231   var escape = app.get('json escape')
232   var replacer = app.get('json replacer');
233   var spaces = app.get('json spaces');
234   var body = stringify(obj, replacer, spaces, escape)
236   // content-type
237   if (!this.get('Content-Type')) {
238     this.set('Content-Type', 'application/json');
239   }
241   return this.send(body);
245  * Send JSON response with JSONP callback support.
247  * Examples:
249  *     res.jsonp(null);
250  *     res.jsonp({ user: 'tj' });
252  * @param {string|number|boolean|object} obj
253  * @public
254  */
256 res.jsonp = function jsonp(obj) {
257   // settings
258   var app = this.app;
259   var escape = app.get('json escape')
260   var replacer = app.get('json replacer');
261   var spaces = app.get('json spaces');
262   var body = stringify(obj, replacer, spaces, escape)
263   var callback = this.req.query[app.get('jsonp callback name')];
265   // content-type
266   if (!this.get('Content-Type')) {
267     this.set('X-Content-Type-Options', 'nosniff');
268     this.set('Content-Type', 'application/json');
269   }
271   // fixup callback
272   if (Array.isArray(callback)) {
273     callback = callback[0];
274   }
276   // jsonp
277   if (typeof callback === 'string' && callback.length !== 0) {
278     this.set('X-Content-Type-Options', 'nosniff');
279     this.set('Content-Type', 'text/javascript');
281     // restrict callback charset
282     callback = callback.replace(/[^\[\]\w$.]/g, '');
284     if (body === undefined) {
285       // empty argument
286       body = ''
287     } else if (typeof body === 'string') {
288       // replace chars not allowed in JavaScript that are in JSON
289       body = body
290         .replace(/\u2028/g, '\\u2028')
291         .replace(/\u2029/g, '\\u2029')
292     }
294     // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
295     // the typeof check is just to reduce client error noise
296     body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
297   }
299   return this.send(body);
303  * Send given HTTP status code.
305  * Sets the response status to `statusCode` and the body of the
306  * response to the standard description from node's http.STATUS_CODES
307  * or the statusCode number if no description.
309  * Examples:
311  *     res.sendStatus(200);
313  * @param {number} statusCode
314  * @public
315  */
317 res.sendStatus = function sendStatus(statusCode) {
318   var body = statuses.message[statusCode] || String(statusCode)
320   this.status(statusCode);
321   this.type('txt');
323   return this.send(body);
327  * Transfer the file at the given `path`.
329  * Automatically sets the _Content-Type_ response header field.
330  * The callback `callback(err)` is invoked when the transfer is complete
331  * or when an error occurs. Be sure to check `res.headersSent`
332  * if you wish to attempt responding, as the header and some data
333  * may have already been transferred.
335  * Options:
337  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
338  *   - `root`     root directory for relative filenames
339  *   - `headers`  object of headers to serve with file
340  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
342  * Other options are passed along to `send`.
344  * Examples:
346  *  The following example illustrates how `res.sendFile()` may
347  *  be used as an alternative for the `static()` middleware for
348  *  dynamic situations. The code backing `res.sendFile()` is actually
349  *  the same code, so HTTP cache support etc is identical.
351  *     app.get('/user/:uid/photos/:file', function(req, res){
352  *       var uid = req.params.uid
353  *         , file = req.params.file;
355  *       req.user.mayViewFilesFrom(uid, function(yes){
356  *         if (yes) {
357  *           res.sendFile('/uploads/' + uid + '/' + file);
358  *         } else {
359  *           res.send(403, 'Sorry! you cant see that.');
360  *         }
361  *       });
362  *     });
364  * @public
365  */
367 res.sendFile = function sendFile(path, options, callback) {
368   var done = callback;
369   var req = this.req;
370   var res = this;
371   var next = req.next;
372   var opts = options || {};
374   if (!path) {
375     throw new TypeError('path argument is required to res.sendFile');
376   }
378   if (typeof path !== 'string') {
379     throw new TypeError('path must be a string to res.sendFile')
380   }
382   // support function as second arg
383   if (typeof options === 'function') {
384     done = options;
385     opts = {};
386   }
388   if (!opts.root && !pathIsAbsolute(path)) {
389     throw new TypeError('path must be absolute or specify root to res.sendFile');
390   }
392   // create file stream
393   var pathname = encodeURI(path);
394   var file = send(req, pathname, opts);
396   // transfer
397   sendfile(res, file, opts, function (err) {
398     if (done) return done(err);
399     if (err && err.code === 'EISDIR') return next();
401     // next() all but write errors
402     if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
403       next(err);
404     }
405   });
409  * Transfer the file at the given `path` as an attachment.
411  * Optionally providing an alternate attachment `filename`,
412  * and optional callback `callback(err)`. The callback is invoked
413  * when the data transfer is complete, or when an error has
414  * occurred. Be sure to check `res.headersSent` if you plan to respond.
416  * Optionally providing an `options` object to use with `res.sendFile()`.
417  * This function will set the `Content-Disposition` header, overriding
418  * any `Content-Disposition` header passed as header options in order
419  * to set the attachment and filename.
421  * This method uses `res.sendFile()`.
423  * @public
424  */
426 res.download = function download (path, filename, options, callback) {
427   var done = callback;
428   var name = filename;
429   var opts = options || null
431   // support function as second or third arg
432   if (typeof filename === 'function') {
433     done = filename;
434     name = null;
435     opts = null
436   } else if (typeof options === 'function') {
437     done = options
438     opts = null
439   }
441   // support optional filename, where options may be in it's place
442   if (typeof filename === 'object' &&
443     (typeof options === 'function' || options === undefined)) {
444     name = null
445     opts = filename
446   }
448   // set Content-Disposition when file is sent
449   var headers = {
450     'Content-Disposition': contentDisposition(name || path)
451   };
453   // merge user-provided headers
454   if (opts && opts.headers) {
455     var keys = Object.keys(opts.headers)
456     for (var i = 0; i < keys.length; i++) {
457       var key = keys[i]
458       if (key.toLowerCase() !== 'content-disposition') {
459         headers[key] = opts.headers[key]
460       }
461     }
462   }
464   // merge user-provided options
465   opts = Object.create(opts)
466   opts.headers = headers
468   // Resolve the full path for sendFile
469   var fullPath = !opts.root
470     ? resolve(path)
471     : path
473   // send file
474   return this.sendFile(fullPath, opts, done)
478  * Set _Content-Type_ response header with `type` through `mime.contentType()`
479  * when it does not contain "/", or set the Content-Type to `type` otherwise.
480  * When no mapping is found though `mime.contentType()`, the type is set to
481  * "application/octet-stream".
483  * Examples:
485  *     res.type('.html');
486  *     res.type('html');
487  *     res.type('json');
488  *     res.type('application/json');
489  *     res.type('png');
491  * @param {String} type
492  * @return {ServerResponse} for chaining
493  * @public
494  */
496 res.contentType =
497 res.type = function contentType(type) {
498   var ct = type.indexOf('/') === -1
499     ? (mime.contentType(type) || 'application/octet-stream')
500     : type;
502   return this.set('Content-Type', ct);
506  * Respond to the Acceptable formats using an `obj`
507  * of mime-type callbacks.
509  * This method uses `req.accepted`, an array of
510  * acceptable types ordered by their quality values.
511  * When "Accept" is not present the _first_ callback
512  * is invoked, otherwise the first match is used. When
513  * no match is performed the server responds with
514  * 406 "Not Acceptable".
516  * Content-Type is set for you, however if you choose
517  * you may alter this within the callback using `res.type()`
518  * or `res.set('Content-Type', ...)`.
520  *    res.format({
521  *      'text/plain': function(){
522  *        res.send('hey');
523  *      },
525  *      'text/html': function(){
526  *        res.send('<p>hey</p>');
527  *      },
529  *      'application/json': function () {
530  *        res.send({ message: 'hey' });
531  *      }
532  *    });
534  * In addition to canonicalized MIME types you may
535  * also use extnames mapped to these types:
537  *    res.format({
538  *      text: function(){
539  *        res.send('hey');
540  *      },
542  *      html: function(){
543  *        res.send('<p>hey</p>');
544  *      },
546  *      json: function(){
547  *        res.send({ message: 'hey' });
548  *      }
549  *    });
551  * By default Express passes an `Error`
552  * with a `.status` of 406 to `next(err)`
553  * if a match is not made. If you provide
554  * a `.default` callback it will be invoked
555  * instead.
557  * @param {Object} obj
558  * @return {ServerResponse} for chaining
559  * @public
560  */
562 res.format = function(obj){
563   var req = this.req;
564   var next = req.next;
566   var keys = Object.keys(obj)
567     .filter(function (v) { return v !== 'default' })
569   var key = keys.length > 0
570     ? req.accepts(keys)
571     : false;
573   this.vary("Accept");
575   if (key) {
576     this.set('Content-Type', normalizeType(key).value);
577     obj[key](req, this, next);
578   } else if (obj.default) {
579     obj.default(req, this, next)
580   } else {
581     next(createError(406, {
582       types: normalizeTypes(keys).map(function (o) { return o.value })
583     }))
584   }
586   return this;
590  * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
592  * @param {String} filename
593  * @return {ServerResponse}
594  * @public
595  */
597 res.attachment = function attachment(filename) {
598   if (filename) {
599     this.type(extname(filename));
600   }
602   this.set('Content-Disposition', contentDisposition(filename));
604   return this;
608  * Append additional header `field` with value `val`.
610  * Example:
612  *    res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
613  *    res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
614  *    res.append('Warning', '199 Miscellaneous warning');
616  * @param {String} field
617  * @param {String|Array} val
618  * @return {ServerResponse} for chaining
619  * @public
620  */
622 res.append = function append(field, val) {
623   var prev = this.get(field);
624   var value = val;
626   if (prev) {
627     // concat the new and prev vals
628     value = Array.isArray(prev) ? prev.concat(val)
629       : Array.isArray(val) ? [prev].concat(val)
630         : [prev, val]
631   }
633   return this.set(field, value);
637  * Set header `field` to `val`, or pass
638  * an object of header fields.
640  * Examples:
642  *    res.set('Foo', ['bar', 'baz']);
643  *    res.set('Accept', 'application/json');
644  *    res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
646  * Aliased as `res.header()`.
648  * When the set header is "Content-Type", the type is expanded to include
649  * the charset if not present using `mime.contentType()`.
651  * @param {String|Object} field
652  * @param {String|Array} val
653  * @return {ServerResponse} for chaining
654  * @public
655  */
657 res.set =
658 res.header = function header(field, val) {
659   if (arguments.length === 2) {
660     var value = Array.isArray(val)
661       ? val.map(String)
662       : String(val);
664     // add charset to content-type
665     if (field.toLowerCase() === 'content-type') {
666       if (Array.isArray(value)) {
667         throw new TypeError('Content-Type cannot be set to an Array');
668       }
669       value = mime.contentType(value)
670     }
672     this.setHeader(field, value);
673   } else {
674     for (var key in field) {
675       this.set(key, field[key]);
676     }
677   }
678   return this;
682  * Get value for header `field`.
684  * @param {String} field
685  * @return {String}
686  * @public
687  */
689 res.get = function(field){
690   return this.getHeader(field);
694  * Clear cookie `name`.
696  * @param {String} name
697  * @param {Object} [options]
698  * @return {ServerResponse} for chaining
699  * @public
700  */
702 res.clearCookie = function clearCookie(name, options) {
703   // Force cookie expiration by setting expires to the past
704   const opts = { path: '/', ...options, expires: new Date(1)};
705   // ensure maxAge is not passed
706   delete opts.maxAge
708   return this.cookie(name, '', opts);
712  * Set cookie `name` to `value`, with the given `options`.
714  * Options:
716  *    - `maxAge`   max-age in milliseconds, converted to `expires`
717  *    - `signed`   sign the cookie
718  *    - `path`     defaults to "/"
720  * Examples:
722  *    // "Remember Me" for 15 minutes
723  *    res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
725  *    // same as above
726  *    res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
728  * @param {String} name
729  * @param {String|Object} value
730  * @param {Object} [options]
731  * @return {ServerResponse} for chaining
732  * @public
733  */
735 res.cookie = function (name, value, options) {
736   var opts = merge({}, options);
737   var secret = this.req.secret;
738   var signed = opts.signed;
740   if (signed && !secret) {
741     throw new Error('cookieParser("secret") required for signed cookies');
742   }
744   var val = typeof value === 'object'
745     ? 'j:' + JSON.stringify(value)
746     : String(value);
748   if (signed) {
749     val = 's:' + sign(val, secret);
750   }
752   if (opts.maxAge != null) {
753     var maxAge = opts.maxAge - 0
755     if (!isNaN(maxAge)) {
756       opts.expires = new Date(Date.now() + maxAge)
757       opts.maxAge = Math.floor(maxAge / 1000)
758     }
759   }
761   if (opts.path == null) {
762     opts.path = '/';
763   }
765   this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
767   return this;
771  * Set the location header to `url`.
773  * The given `url` can also be "back", which redirects
774  * to the _Referrer_ or _Referer_ headers or "/".
776  * Examples:
778  *    res.location('/foo/bar').;
779  *    res.location('http://example.com');
780  *    res.location('../login');
782  * @param {String} url
783  * @return {ServerResponse} for chaining
784  * @public
785  */
787 res.location = function location(url) {
788   return this.set('Location', encodeUrl(url));
792  * Redirect to the given `url` with optional response `status`
793  * defaulting to 302.
795  * Examples:
797  *    res.redirect('/foo/bar');
798  *    res.redirect('http://example.com');
799  *    res.redirect(301, 'http://example.com');
800  *    res.redirect('../login'); // /blog/post/1 -> /blog/login
802  * @public
803  */
805 res.redirect = function redirect(url) {
806   var address = url;
807   var body;
808   var status = 302;
810   // allow status / url
811   if (arguments.length === 2) {
812     status = arguments[0]
813     address = arguments[1]
814   }
816   // Set location header
817   address = this.location(address).get('Location');
819   // Support text/{plain,html} by default
820   this.format({
821     text: function(){
822       body = statuses.message[status] + '. Redirecting to ' + address
823     },
825     html: function(){
826       var u = escapeHtml(address);
827       body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
828     },
830     default: function(){
831       body = '';
832     }
833   });
835   // Respond
836   this.status(status);
837   this.set('Content-Length', Buffer.byteLength(body));
839   if (this.req.method === 'HEAD') {
840     this.end();
841   } else {
842     this.end(body);
843   }
847  * Add `field` to Vary. If already present in the Vary set, then
848  * this call is simply ignored.
850  * @param {Array|String} field
851  * @return {ServerResponse} for chaining
852  * @public
853  */
855 res.vary = function(field){
856   vary(this, field);
858   return this;
862  * Render `view` with the given `options` and optional callback `fn`.
863  * When a callback function is given a response will _not_ be made
864  * automatically, otherwise a response of _200_ and _text/html_ is given.
866  * Options:
868  *  - `cache`     boolean hinting to the engine it should cache
869  *  - `filename`  filename of the view being rendered
871  * @public
872  */
874 res.render = function render(view, options, callback) {
875   var app = this.req.app;
876   var done = callback;
877   var opts = options || {};
878   var req = this.req;
879   var self = this;
881   // support callback function as second arg
882   if (typeof options === 'function') {
883     done = options;
884     opts = {};
885   }
887   // merge res.locals
888   opts._locals = self.locals;
890   // default callback to respond
891   done = done || function (err, str) {
892     if (err) return req.next(err);
893     self.send(str);
894   };
896   // render
897   app.render(view, opts, done);
900 // pipe the send file stream
901 function sendfile(res, file, options, callback) {
902   var done = false;
903   var streaming;
905   // request aborted
906   function onaborted() {
907     if (done) return;
908     done = true;
910     var err = new Error('Request aborted');
911     err.code = 'ECONNABORTED';
912     callback(err);
913   }
915   // directory
916   function ondirectory() {
917     if (done) return;
918     done = true;
920     var err = new Error('EISDIR, read');
921     err.code = 'EISDIR';
922     callback(err);
923   }
925   // errors
926   function onerror(err) {
927     if (done) return;
928     done = true;
929     callback(err);
930   }
932   // ended
933   function onend() {
934     if (done) return;
935     done = true;
936     callback();
937   }
939   // file
940   function onfile() {
941     streaming = false;
942   }
944   // finished
945   function onfinish(err) {
946     if (err && err.code === 'ECONNRESET') return onaborted();
947     if (err) return onerror(err);
948     if (done) return;
950     setImmediate(function () {
951       if (streaming !== false && !done) {
952         onaborted();
953         return;
954       }
956       if (done) return;
957       done = true;
958       callback();
959     });
960   }
962   // streaming
963   function onstream() {
964     streaming = true;
965   }
967   file.on('directory', ondirectory);
968   file.on('end', onend);
969   file.on('error', onerror);
970   file.on('file', onfile);
971   file.on('stream', onstream);
972   onFinished(res, onfinish);
974   if (options.headers) {
975     // set headers on successful transfer
976     file.on('headers', function headers(res) {
977       var obj = options.headers;
978       var keys = Object.keys(obj);
980       for (var i = 0; i < keys.length; i++) {
981         var k = keys[i];
982         res.setHeader(k, obj[k]);
983       }
984     });
985   }
987   // pipe
988   file.pipe(res);
992  * Stringify JSON, like JSON.stringify, but v8 optimized, with the
993  * ability to escape characters that can trigger HTML sniffing.
995  * @param {*} value
996  * @param {function} replacer
997  * @param {number} spaces
998  * @param {boolean} escape
999  * @returns {string}
1000  * @private
1001  */
1003 function stringify (value, replacer, spaces, escape) {
1004   // v8 checks arguments.length for optimizing simple call
1005   // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1006   var json = replacer || spaces
1007     ? JSON.stringify(value, replacer, spaces)
1008     : JSON.stringify(value);
1010   if (escape && typeof json === 'string') {
1011     json = json.replace(/[<>&]/g, function (c) {
1012       switch (c.charCodeAt(0)) {
1013         case 0x3c:
1014           return '\\u003c'
1015         case 0x3e:
1016           return '\\u003e'
1017         case 0x26:
1018           return '\\u0026'
1019         /* istanbul ignore next: unreachable default */
1020         default:
1021           return c
1022       }
1023     })
1024   }
1026   return json