Skip to content

Policy at construction

A mapping describes what to produce. Where requests may go, how long to wait, which headers reach the wire: those are facts about a deployment, and the Mapper family keeps them out of mapping documents on principle. This page explains the principle, using the request plugin as the worked example.

A mapping document and a deployment configuration usually have different authors. Mappings travel: they are data, they get stored, submitted, shared, and, behind a service like mapper-http, authored by callers. Configuration does not travel: it is written by whoever operates the process, once, at construction.

That difference is why createRequest(config) exists. Everything security-relevant (timeoutMs, allowHeaders, checkUrl, maxResponseBytes) is fixed in the factory’s closure. A descriptor can describe any request it likes; it cannot loosen the policy it runs under, because the policy was decided before any mapping arrived. The alternative, policy keywords in the descriptor, would mean every mapping author is a policy author, precisely backwards for documents designed to be passed around.

A mapping that carries no policy runs anywhere. The same document works in a test harness with a fixture server, on a laptop against staging, and in production behind strict egress, because “where may requests go” was never the document’s business. Policy differences between those environments live in three lines of construction code, not in three variants of every mapping.

This is the same shape as the rest of the extension system: mappings name capabilities (request, slugify), deployments decide what those names mean and how far they reach. The names are portable; the reach is local.

The request plugin’s fixed behaviors follow one rule: when data steers a request, hold the boundary at a point data cannot reach.

  • Redirects are refused by default. The upstream chooses the redirect target; following it blindly would let a response steer the request somewhere the deployment never approved. The one relaxation is itself policy at construction: redirect: 'follow' follows a bounded chain (GET-only, capped hops, same-origin plus the https upgrade of the same host), and every hop’s target re-passes checkUrl before it is fetched — the response proposes, the deployment’s own policy still disposes.
  • checkUrl sees the final URL before any connection. For deployments that fetch URLs arriving as data, the useful posture is a network-position guard (refuse loopback, private, and link-local ranges) rather than a site allowlist that needs a redeploy per new target.
  • Time and size are bounded because unbounded reads are a policy decision nobody made.

A string check on the URL is the cheap gate, and it is honest about its limits: it cannot see what a hostname will resolve to. Network egress rules are the robust wall; checkUrl keeps well-behaved mappings from ever testing it. Belt, then braces: configuration first, network second, and nothing in the mapping deciding either.