update history.md for acceptParams change (#6177)
[express.git] / lib / response.js
blobe439a06ae8648aa3072be1cb43b299a1f6351f6d
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 contentDisposition = require('content-disposition');
16 var createError = require('http-errors')
17 var encodeUrl = require('encodeurl');
18 var escapeHtml = require('escape-html');
19 var http = require('http');
20 var onFinished = require('on-finished');
21 var mime = require('mime-types')
22 var path = require('path');
23 var pathIsAbsolute = require('path').isAbsolute;
24 var statuses = require('statuses')
25 var merge = require('utils-merge');
26 var sign = require('cookie-signature').sign;
27 var normalizeType = require('./utils').normalizeType;
28 var normalizeTypes = require('./utils').normalizeTypes;
29 var setCharset = require('./utils').setCharset;
30 var cookie = require('cookie');
31 var send = require('send');
32 var extname = path.extname;
33 var resolve = path.resolve;
34 var vary = require('vary');
36 /**
37  * Response prototype.
38  * @public
39  */
41 var res = Object.create(http.ServerResponse.prototype)
43 /**
44  * Module exports.
45  * @public
46  */
48 module.exports = res
50 /**
51  * Set the HTTP status code for the response.
52  *
53  * Expects an integer value between 100 and 999 inclusive.
54  * Throws an error if the provided status code is not an integer or if it's outside the allowable range.
55  *
56  * @param {number} code - The HTTP status code to set.
57  * @return {ServerResponse} - Returns itself for chaining methods.
58  * @throws {TypeError} If `code` is not an integer.
59  * @throws {RangeError} If `code` is outside the range 100 to 999.
60  * @public
61  */
63 res.status = function status(code) {
64   // Check if the status code is not an integer
65   if (!Number.isInteger(code)) {
66     throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);
67   }
68   // Check if the status code is outside of Node's valid range
69   if (code < 100 || code > 999) {
70     throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`);
71   }
73   this.statusCode = code;
74   return this;
77 /**
78  * Set Link header field with the given `links`.
79  *
80  * Examples:
81  *
82  *    res.links({
83  *      next: 'http://api.example.com/users?page=2',
84  *      last: 'http://api.example.com/users?page=5'
85  *    });
86  *
87  * @param {Object} links
88  * @return {ServerResponse}
89  * @public
90  */
92 res.links = function(links){
93   var link = this.get('Link') || '';
94   if (link) link += ', ';
95   return this.set('Link', link + Object.keys(links).map(function(rel){
96     return '<' + links[rel] + '>; rel="' + rel + '"';
97   }).join(', '));
101  * Send a response.
103  * Examples:
105  *     res.send(Buffer.from('wahoo'));
106  *     res.send({ some: 'json' });
107  *     res.send('<p>some html</p>');
109  * @param {string|number|boolean|object|Buffer} body
110  * @public
111  */
113 res.send = function send(body) {
114   var chunk = body;
115   var encoding;
116   var req = this.req;
117   var type;
119   // settings
120   var app = this.app;
122   switch (typeof chunk) {
123     // string defaulting to html
124     case 'string':
125       if (!this.get('Content-Type')) {
126         this.type('html');
127       }
128       break;
129     case 'boolean':
130     case 'number':
131     case 'object':
132       if (chunk === null) {
133         chunk = '';
134       } else if (Buffer.isBuffer(chunk)) {
135         if (!this.get('Content-Type')) {
136           this.type('bin');
137         }
138       } else {
139         return this.json(chunk);
140       }
141       break;
142   }
144   // write strings in utf-8
145   if (typeof chunk === 'string') {
146     encoding = 'utf8';
147     type = this.get('Content-Type');
149     // reflect this in content-type
150     if (typeof type === 'string') {
151       this.set('Content-Type', setCharset(type, 'utf-8'));
152     }
153   }
155   // determine if ETag should be generated
156   var etagFn = app.get('etag fn')
157   var generateETag = !this.get('ETag') && typeof etagFn === 'function'
159   // populate Content-Length
160   var len
161   if (chunk !== undefined) {
162     if (Buffer.isBuffer(chunk)) {
163       // get length of Buffer
164       len = chunk.length
165     } else if (!generateETag && chunk.length < 1000) {
166       // just calculate length when no ETag + small chunk
167       len = Buffer.byteLength(chunk, encoding)
168     } else {
169       // convert chunk to Buffer and calculate
170       chunk = Buffer.from(chunk, encoding)
171       encoding = undefined;
172       len = chunk.length
173     }
175     this.set('Content-Length', len);
176   }
178   // populate ETag
179   var etag;
180   if (generateETag && len !== undefined) {
181     if ((etag = etagFn(chunk, encoding))) {
182       this.set('ETag', etag);
183     }
184   }
186   // freshness
187   if (req.fresh) this.status(304);
189   // strip irrelevant headers
190   if (204 === this.statusCode || 304 === this.statusCode) {
191     this.removeHeader('Content-Type');
192     this.removeHeader('Content-Length');
193     this.removeHeader('Transfer-Encoding');
194     chunk = '';
195   }
197   // alter headers for 205
198   if (this.statusCode === 205) {
199     this.set('Content-Length', '0')
200     this.removeHeader('Transfer-Encoding')
201     chunk = ''
202   }
204   if (req.method === 'HEAD') {
205     // skip body for HEAD
206     this.end();
207   } else {
208     // respond
209     this.end(chunk, encoding);
210   }
212   return this;
216  * Send JSON response.
218  * Examples:
220  *     res.json(null);
221  *     res.json({ user: 'tj' });
223  * @param {string|number|boolean|object} obj
224  * @public
225  */
227 res.json = function json(obj) {
228   // settings
229   var app = this.app;
230   var escape = app.get('json escape')
231   var replacer = app.get('json replacer');
232   var spaces = app.get('json spaces');
233   var body = stringify(obj, replacer, spaces, escape)
235   // content-type
236   if (!this.get('Content-Type')) {
237     this.set('Content-Type', 'application/json');
238   }
240   return this.send(body);
244  * Send JSON response with JSONP callback support.
246  * Examples:
248  *     res.jsonp(null);
249  *     res.jsonp({ user: 'tj' });
251  * @param {string|number|boolean|object} obj
252  * @public
253  */
255 res.jsonp = function jsonp(obj) {
256   // settings
257   var app = this.app;
258   var escape = app.get('json escape')
259   var replacer = app.get('json replacer');
260   var spaces = app.get('json spaces');
261   var body = stringify(obj, replacer, spaces, escape)
262   var callback = this.req.query[app.get('jsonp callback name')];
264   // content-type
265   if (!this.get('Content-Type')) {
266     this.set('X-Content-Type-Options', 'nosniff');
267     this.set('Content-Type', 'application/json');
268   }
270   // fixup callback
271   if (Array.isArray(callback)) {
272     callback = callback[0];
273   }
275   // jsonp
276   if (typeof callback === 'string' && callback.length !== 0) {
277     this.set('X-Content-Type-Options', 'nosniff');
278     this.set('Content-Type', 'text/javascript');
280     // restrict callback charset
281     callback = callback.replace(/[^\[\]\w$.]/g, '');
283     if (body === undefined) {
284       // empty argument
285       body = ''
286     } else if (typeof body === 'string') {
287       // replace chars not allowed in JavaScript that are in JSON
288       body = body
289         .replace(/\u2028/g, '\\u2028')
290         .replace(/\u2029/g, '\\u2029')
291     }
293     // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
294     // the typeof check is just to reduce client error noise
295     body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
296   }
298   return this.send(body);
302  * Send given HTTP status code.
304  * Sets the response status to `statusCode` and the body of the
305  * response to the standard description from node's http.STATUS_CODES
306  * or the statusCode number if no description.
308  * Examples:
310  *     res.sendStatus(200);
312  * @param {number} statusCode
313  * @public
314  */
316 res.sendStatus = function sendStatus(statusCode) {
317   var body = statuses.message[statusCode] || String(statusCode)
319   this.status(statusCode);
320   this.type('txt');
322   return this.send(body);
326  * Transfer the file at the given `path`.
328  * Automatically sets the _Content-Type_ response header field.
329  * The callback `callback(err)` is invoked when the transfer is complete
330  * or when an error occurs. Be sure to check `res.headersSent`
331  * if you wish to attempt responding, as the header and some data
332  * may have already been transferred.
334  * Options:
336  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
337  *   - `root`     root directory for relative filenames
338  *   - `headers`  object of headers to serve with file
339  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
341  * Other options are passed along to `send`.
343  * Examples:
345  *  The following example illustrates how `res.sendFile()` may
346  *  be used as an alternative for the `static()` middleware for
347  *  dynamic situations. The code backing `res.sendFile()` is actually
348  *  the same code, so HTTP cache support etc is identical.
350  *     app.get('/user/:uid/photos/:file', function(req, res){
351  *       var uid = req.params.uid
352  *         , file = req.params.file;
354  *       req.user.mayViewFilesFrom(uid, function(yes){
355  *         if (yes) {
356  *           res.sendFile('/uploads/' + uid + '/' + file);
357  *         } else {
358  *           res.send(403, 'Sorry! you cant see that.');
359  *         }
360  *       });
361  *     });
363  * @public
364  */
366 res.sendFile = function sendFile(path, options, callback) {
367   var done = callback;
368   var req = this.req;
369   var res = this;
370   var next = req.next;
371   var opts = options || {};
373   if (!path) {
374     throw new TypeError('path argument is required to res.sendFile');
375   }
377   if (typeof path !== 'string') {
378     throw new TypeError('path must be a string to res.sendFile')
379   }
381   // support function as second arg
382   if (typeof options === 'function') {
383     done = options;
384     opts = {};
385   }
387   if (!opts.root && !pathIsAbsolute(path)) {
388     throw new TypeError('path must be absolute or specify root to res.sendFile');
389   }
391   // create file stream
392   var pathname = encodeURI(path);
393   var file = send(req, pathname, opts);
395   // transfer
396   sendfile(res, file, opts, function (err) {
397     if (done) return done(err);
398     if (err && err.code === 'EISDIR') return next();
400     // next() all but write errors
401     if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
402       next(err);
403     }
404   });
408  * Transfer the file at the given `path` as an attachment.
410  * Optionally providing an alternate attachment `filename`,
411  * and optional callback `callback(err)`. The callback is invoked
412  * when the data transfer is complete, or when an error has
413  * occurred. Be sure to check `res.headersSent` if you plan to respond.
415  * Optionally providing an `options` object to use with `res.sendFile()`.
416  * This function will set the `Content-Disposition` header, overriding
417  * any `Content-Disposition` header passed as header options in order
418  * to set the attachment and filename.
420  * This method uses `res.sendFile()`.
422  * @public
423  */
425 res.download = function download (path, filename, options, callback) {
426   var done = callback;
427   var name = filename;
428   var opts = options || null
430   // support function as second or third arg
431   if (typeof filename === 'function') {
432     done = filename;
433     name = null;
434     opts = null
435   } else if (typeof options === 'function') {
436     done = options
437     opts = null
438   }
440   // support optional filename, where options may be in it's place
441   if (typeof filename === 'object' &&
442     (typeof options === 'function' || options === undefined)) {
443     name = null
444     opts = filename
445   }
447   // set Content-Disposition when file is sent
448   var headers = {
449     'Content-Disposition': contentDisposition(name || path)
450   };
452   // merge user-provided headers
453   if (opts && opts.headers) {
454     var keys = Object.keys(opts.headers)
455     for (var i = 0; i < keys.length; i++) {
456       var key = keys[i]
457       if (key.toLowerCase() !== 'content-disposition') {
458         headers[key] = opts.headers[key]
459       }
460     }
461   }
463   // merge user-provided options
464   opts = Object.create(opts)
465   opts.headers = headers
467   // Resolve the full path for sendFile
468   var fullPath = !opts.root
469     ? resolve(path)
470     : path
472   // send file
473   return this.sendFile(fullPath, opts, done)
477  * Set _Content-Type_ response header with `type` through `mime.contentType()`
478  * when it does not contain "/", or set the Content-Type to `type` otherwise.
479  * When no mapping is found though `mime.contentType()`, the type is set to
480  * "application/octet-stream".
482  * Examples:
484  *     res.type('.html');
485  *     res.type('html');
486  *     res.type('json');
487  *     res.type('application/json');
488  *     res.type('png');
490  * @param {String} type
491  * @return {ServerResponse} for chaining
492  * @public
493  */
495 res.contentType =
496 res.type = function contentType(type) {
497   var ct = type.indexOf('/') === -1
498     ? (mime.contentType(type) || 'application/octet-stream')
499     : type;
501   return this.set('Content-Type', ct);
505  * Respond to the Acceptable formats using an `obj`
506  * of mime-type callbacks.
508  * This method uses `req.accepted`, an array of
509  * acceptable types ordered by their quality values.
510  * When "Accept" is not present the _first_ callback
511  * is invoked, otherwise the first match is used. When
512  * no match is performed the server responds with
513  * 406 "Not Acceptable".
515  * Content-Type is set for you, however if you choose
516  * you may alter this within the callback using `res.type()`
517  * or `res.set('Content-Type', ...)`.
519  *    res.format({
520  *      'text/plain': function(){
521  *        res.send('hey');
522  *      },
524  *      'text/html': function(){
525  *        res.send('<p>hey</p>');
526  *      },
528  *      'application/json': function () {
529  *        res.send({ message: 'hey' });
530  *      }
531  *    });
533  * In addition to canonicalized MIME types you may
534  * also use extnames mapped to these types:
536  *    res.format({
537  *      text: function(){
538  *        res.send('hey');
539  *      },
541  *      html: function(){
542  *        res.send('<p>hey</p>');
543  *      },
545  *      json: function(){
546  *        res.send({ message: 'hey' });
547  *      }
548  *    });
550  * By default Express passes an `Error`
551  * with a `.status` of 406 to `next(err)`
552  * if a match is not made. If you provide
553  * a `.default` callback it will be invoked
554  * instead.
556  * @param {Object} obj
557  * @return {ServerResponse} for chaining
558  * @public
559  */
561 res.format = function(obj){
562   var req = this.req;
563   var next = req.next;
565   var keys = Object.keys(obj)
566     .filter(function (v) { return v !== 'default' })
568   var key = keys.length > 0
569     ? req.accepts(keys)
570     : false;
572   this.vary("Accept");
574   if (key) {
575     this.set('Content-Type', normalizeType(key).value);
576     obj[key](req, this, next);
577   } else if (obj.default) {
578     obj.default(req, this, next)
579   } else {
580     next(createError(406, {
581       types: normalizeTypes(keys).map(function (o) { return o.value })
582     }))
583   }
585   return this;
589  * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
591  * @param {String} filename
592  * @return {ServerResponse}
593  * @public
594  */
596 res.attachment = function attachment(filename) {
597   if (filename) {
598     this.type(extname(filename));
599   }
601   this.set('Content-Disposition', contentDisposition(filename));
603   return this;
607  * Append additional header `field` with value `val`.
609  * Example:
611  *    res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
612  *    res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
613  *    res.append('Warning', '199 Miscellaneous warning');
615  * @param {String} field
616  * @param {String|Array} val
617  * @return {ServerResponse} for chaining
618  * @public
619  */
621 res.append = function append(field, val) {
622   var prev = this.get(field);
623   var value = val;
625   if (prev) {
626     // concat the new and prev vals
627     value = Array.isArray(prev) ? prev.concat(val)
628       : Array.isArray(val) ? [prev].concat(val)
629         : [prev, val]
630   }
632   return this.set(field, value);
636  * Set header `field` to `val`, or pass
637  * an object of header fields.
639  * Examples:
641  *    res.set('Foo', ['bar', 'baz']);
642  *    res.set('Accept', 'application/json');
643  *    res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
645  * Aliased as `res.header()`.
647  * When the set header is "Content-Type", the type is expanded to include
648  * the charset if not present using `mime.contentType()`.
650  * @param {String|Object} field
651  * @param {String|Array} val
652  * @return {ServerResponse} for chaining
653  * @public
654  */
656 res.set =
657 res.header = function header(field, val) {
658   if (arguments.length === 2) {
659     var value = Array.isArray(val)
660       ? val.map(String)
661       : String(val);
663     // add charset to content-type
664     if (field.toLowerCase() === 'content-type') {
665       if (Array.isArray(value)) {
666         throw new TypeError('Content-Type cannot be set to an Array');
667       }
668       value = mime.contentType(value)
669     }
671     this.setHeader(field, value);
672   } else {
673     for (var key in field) {
674       this.set(key, field[key]);
675     }
676   }
677   return this;
681  * Get value for header `field`.
683  * @param {String} field
684  * @return {String}
685  * @public
686  */
688 res.get = function(field){
689   return this.getHeader(field);
693  * Clear cookie `name`.
695  * @param {String} name
696  * @param {Object} [options]
697  * @return {ServerResponse} for chaining
698  * @public
699  */
701 res.clearCookie = function clearCookie(name, options) {
702   // Force cookie expiration by setting expires to the past
703   const opts = { path: '/', ...options, expires: new Date(1)};
704   // ensure maxAge is not passed
705   delete opts.maxAge
707   return this.cookie(name, '', opts);
711  * Set cookie `name` to `value`, with the given `options`.
713  * Options:
715  *    - `maxAge`   max-age in milliseconds, converted to `expires`
716  *    - `signed`   sign the cookie
717  *    - `path`     defaults to "/"
719  * Examples:
721  *    // "Remember Me" for 15 minutes
722  *    res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
724  *    // same as above
725  *    res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
727  * @param {String} name
728  * @param {String|Object} value
729  * @param {Object} [options]
730  * @return {ServerResponse} for chaining
731  * @public
732  */
734 res.cookie = function (name, value, options) {
735   var opts = merge({}, options);
736   var secret = this.req.secret;
737   var signed = opts.signed;
739   if (signed && !secret) {
740     throw new Error('cookieParser("secret") required for signed cookies');
741   }
743   var val = typeof value === 'object'
744     ? 'j:' + JSON.stringify(value)
745     : String(value);
747   if (signed) {
748     val = 's:' + sign(val, secret);
749   }
751   if (opts.maxAge != null) {
752     var maxAge = opts.maxAge - 0
754     if (!isNaN(maxAge)) {
755       opts.expires = new Date(Date.now() + maxAge)
756       opts.maxAge = Math.floor(maxAge / 1000)
757     }
758   }
760   if (opts.path == null) {
761     opts.path = '/';
762   }
764   this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
766   return this;
770  * Set the location header to `url`.
772  * The given `url` can also be "back", which redirects
773  * to the _Referrer_ or _Referer_ headers or "/".
775  * Examples:
777  *    res.location('/foo/bar').;
778  *    res.location('http://example.com');
779  *    res.location('../login');
781  * @param {String} url
782  * @return {ServerResponse} for chaining
783  * @public
784  */
786 res.location = function location(url) {
787   return this.set('Location', encodeUrl(url));
791  * Redirect to the given `url` with optional response `status`
792  * defaulting to 302.
794  * Examples:
796  *    res.redirect('/foo/bar');
797  *    res.redirect('http://example.com');
798  *    res.redirect(301, 'http://example.com');
799  *    res.redirect('../login'); // /blog/post/1 -> /blog/login
801  * @public
802  */
804 res.redirect = function redirect(url) {
805   var address = url;
806   var body;
807   var status = 302;
809   // allow status / url
810   if (arguments.length === 2) {
811     status = arguments[0]
812     address = arguments[1]
813   }
815   // Set location header
816   address = this.location(address).get('Location');
818   // Support text/{plain,html} by default
819   this.format({
820     text: function(){
821       body = statuses.message[status] + '. Redirecting to ' + address
822     },
824     html: function(){
825       var u = escapeHtml(address);
826       body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
827     },
829     default: function(){
830       body = '';
831     }
832   });
834   // Respond
835   this.status(status);
836   this.set('Content-Length', Buffer.byteLength(body));
838   if (this.req.method === 'HEAD') {
839     this.end();
840   } else {
841     this.end(body);
842   }
846  * Add `field` to Vary. If already present in the Vary set, then
847  * this call is simply ignored.
849  * @param {Array|String} field
850  * @return {ServerResponse} for chaining
851  * @public
852  */
854 res.vary = function(field){
855   vary(this, field);
857   return this;
861  * Render `view` with the given `options` and optional callback `fn`.
862  * When a callback function is given a response will _not_ be made
863  * automatically, otherwise a response of _200_ and _text/html_ is given.
865  * Options:
867  *  - `cache`     boolean hinting to the engine it should cache
868  *  - `filename`  filename of the view being rendered
870  * @public
871  */
873 res.render = function render(view, options, callback) {
874   var app = this.req.app;
875   var done = callback;
876   var opts = options || {};
877   var req = this.req;
878   var self = this;
880   // support callback function as second arg
881   if (typeof options === 'function') {
882     done = options;
883     opts = {};
884   }
886   // merge res.locals
887   opts._locals = self.locals;
889   // default callback to respond
890   done = done || function (err, str) {
891     if (err) return req.next(err);
892     self.send(str);
893   };
895   // render
896   app.render(view, opts, done);
899 // pipe the send file stream
900 function sendfile(res, file, options, callback) {
901   var done = false;
902   var streaming;
904   // request aborted
905   function onaborted() {
906     if (done) return;
907     done = true;
909     var err = new Error('Request aborted');
910     err.code = 'ECONNABORTED';
911     callback(err);
912   }
914   // directory
915   function ondirectory() {
916     if (done) return;
917     done = true;
919     var err = new Error('EISDIR, read');
920     err.code = 'EISDIR';
921     callback(err);
922   }
924   // errors
925   function onerror(err) {
926     if (done) return;
927     done = true;
928     callback(err);
929   }
931   // ended
932   function onend() {
933     if (done) return;
934     done = true;
935     callback();
936   }
938   // file
939   function onfile() {
940     streaming = false;
941   }
943   // finished
944   function onfinish(err) {
945     if (err && err.code === 'ECONNRESET') return onaborted();
946     if (err) return onerror(err);
947     if (done) return;
949     setImmediate(function () {
950       if (streaming !== false && !done) {
951         onaborted();
952         return;
953       }
955       if (done) return;
956       done = true;
957       callback();
958     });
959   }
961   // streaming
962   function onstream() {
963     streaming = true;
964   }
966   file.on('directory', ondirectory);
967   file.on('end', onend);
968   file.on('error', onerror);
969   file.on('file', onfile);
970   file.on('stream', onstream);
971   onFinished(res, onfinish);
973   if (options.headers) {
974     // set headers on successful transfer
975     file.on('headers', function headers(res) {
976       var obj = options.headers;
977       var keys = Object.keys(obj);
979       for (var i = 0; i < keys.length; i++) {
980         var k = keys[i];
981         res.setHeader(k, obj[k]);
982       }
983     });
984   }
986   // pipe
987   file.pipe(res);
991  * Stringify JSON, like JSON.stringify, but v8 optimized, with the
992  * ability to escape characters that can trigger HTML sniffing.
994  * @param {*} value
995  * @param {function} replacer
996  * @param {number} spaces
997  * @param {boolean} escape
998  * @returns {string}
999  * @private
1000  */
1002 function stringify (value, replacer, spaces, escape) {
1003   // v8 checks arguments.length for optimizing simple call
1004   // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1005   var json = replacer || spaces
1006     ? JSON.stringify(value, replacer, spaces)
1007     : JSON.stringify(value);
1009   if (escape && typeof json === 'string') {
1010     json = json.replace(/[<>&]/g, function (c) {
1011       switch (c.charCodeAt(0)) {
1012         case 0x3c:
1013           return '\\u003c'
1014         case 0x3e:
1015           return '\\u003e'
1016         case 0x26:
1017           return '\\u0026'
1018         /* istanbul ignore next: unreachable default */
1019         default:
1020           return c
1021       }
1022     })
1023   }
1025   return json