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.

lib/request.js
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 --

lib/request.js
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:

lib/request.js
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:

  1. Type dispatch. Strings default the Content-Type to html; buffers to bin; objects, numbers, and booleans are rerouted to res.json.
  2. Content-Length. Computed from the body unless Transfer-Encoding is already set (the two headers are mutually exclusive).
  3. ETag. Generated by the app's compiled etag fn -- weak by default -- unless the handler already set one:
lib/response.js
// determine if ETag should be generated
var etagFn = app.get('etag fn')
var generateETag = !this.get('ETag') && typeof etagFn === 'function'
  1. Freshness collapse to 304. If the client's validators still match, the status flips and the body is dropped:
lib/response.js
// 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 = '';
}
  1. 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.

Sources: lib/request.js, lib/response.js · last synced 2026-07-27 · a371447 · version 5.2.1