Express never wraps Node's request and response objects -- it re-parents them. lib/request.js builds a prototype on top of http.IncomingMessage.prototype, lib/response.js on top of http.ServerResponse.prototype, and app.handle swaps them in per request with Object.setPrototypeOf (see the request lifecycle). Every Express-specific member is a plain property or getter on those two prototypes; everything Node provides still works underneath.
var req = Object.create(http.IncomingMessage.prototype)
Request: lazy getters wired to app settings
Most req extensions are getters, so nothing is computed until a handler asks. The recurring pattern: read a compiled function from the app's settings, apply it to raw request state. req.query is the clearest case --
defineGetter(req, 'query', function query(){
var queryparse = this.app.get('query parser fn');
if (!queryparse) {
// parsing is disabled
return Object.create(null);
}
var querystring = parse(this).query;
return queryparse(querystring);
});
-- which is why the query parser setting (simple by default, extended for nested qs syntax) changes req.query's shape app-wide; test/req.query.js has a describe block per mode.
The proxy-sensitive getters (req.protocol, req.ip, req.ips, req.host, req.hostname) all consult trust proxy fn before believing any X-Forwarded-* header:
defineGetter(req, 'protocol', function protocol(){
var proto = this.socket.encrypted
? 'https'
: 'http';
var trust = this.app.get('trust proxy fn');
if (!trust(this.socket.remoteAddress, 0)) {
return proto;
}
// Note: X-Forwarded-Proto is normally only ever a
// single value, but this is to be safe.
var header = this.get('X-Forwarded-Proto') || proto
// …
});
With the default trust proxy: false, the compiled trust function returns false for everything, so headers from clients are ignored -- spoofing X-Forwarded-Proto does nothing until an operator opts in.
Content negotiation (req.accepts, req.acceptsEncodings, req.acceptsCharsets, req.acceptsLanguages) and type checking (req.is) delegate to the accepts and type-is packages -- receipts on the content negotiation page. req.fresh combines method, status, and validator headers via the fresh package and is the hinge of Express's built-in 304 behavior, below.
Response: the res.send pipeline
res.send is the funnel almost every response goes through (res.json stringifies and calls it; res.sendStatus, res.jsonp, and the default res.render callback do too). Its stages, in code order in lib/response.js:
- Type dispatch. Strings default the Content-Type to
html; buffers tobin; objects, numbers, and booleans are rerouted tores.json. - Content-Length. Computed from the body unless
Transfer-Encodingis already set (the two headers are mutually exclusive). - ETag. Generated by the app's compiled
etag fn-- weak by default -- unless the handler already set one:
// determine if ETag should be generated
var etagFn = app.get('etag fn')
var generateETag = !this.get('ETag') && typeof etagFn === 'function'
- Freshness collapse to 304. If the client's validators still match, the status flips and the body is dropped:
// freshness
if (req.fresh) this.status(304);
// strip irrelevant headers
if (204 === this.statusCode || 304 === this.statusCode) {
this.removeHeader('Content-Type');
this.removeHeader('Content-Length');
this.removeHeader('Transfer-Encoding');
chunk = '';
}
- HEAD short-circuit. Headers only, no body.
The observable contract -- exact weak ETag values, 304-on-If-None-Match, no freshness check outside 2xx/304 -- is pinned in test/res.send.js and test/req.fresh.js.
res.json adds only serialization on top: it reads the json replacer, json spaces, and json escape settings, stringifies (replacing <, >, & with \u003c-style Unicode escapes when json escape is on, an anti-sniffing measure), sets Content-Type: application/json, and calls res.send.
Files, cookies, redirects
res.sendFile builds a stream from the send package and pipes it, wiring the app's etag setting through (opts.etag = this.app.enabled('etag')). Its 60-line sendfile helper exists to translate stream events into a single callback -- including the subtle case where the response finishes without the file ever streaming, which is reported as ECONNABORTED. res.download is sendFile plus a Content-Disposition: attachment header built by content-disposition.
res.cookie serializes with the cookie package, signs with cookie-signature when signed: true (requiring the cookie-parser middleware to have provided req.secret), converts maxAge in milliseconds to both Expires and Max-Age, and appends to Set-Cookie. res.clearCookie is res.cookie with a forced past expiry.
res.redirect defaults to 302, sets Location (URL-encoded via encodeurl), and content-negotiates its own body -- plain text for text clients, a small HTML page with the target escaped via escape-html for browsers -- using the same res.format machinery available to user code.
res.format picks a callback by req.accepts and calls next with a 406 http-errors error (carrying the list of representable types) when nothing matches and no default is given.
Dependency receipts for the packages named here are on cookies, ETags, and header helpers.