We built a headless browser worker that renders our own pages for screenshots and link checks. Here is the design decision I keep coming back to.
The usual approach
interface ContextOptions {
viewport: { width: number; height: number };
userAgent: string;
proxy?: ProxyConfig; // defaults to none
credentials?: Credentials; // defaults to none
}
This is fine, and it is what most libraries do. The proxy is off unless you set it. The credentials are absent unless you supply them.
Why we did not do that
A configuration option that must never be set is a configuration option that will eventually be set.
Not maliciously. Someone six months from now has a legitimate-sounding reason — a rate limit, a geo-restricted page, a login wall on a page we genuinely own — and the option is right there with a sensible-looking type. The code review is one line. Nobody remembers the argument that produced the default.
What we shipped instead
export interface ContextOptions {
readonly viewport: { readonly width: number; readonly height: number };
readonly userAgent: string;
readonly egressAllowlist: readonly string[];
readonly timeoutMs: number;
}
Four fields. There is no proxy field and no credential field anywhere in the package. A driver implementation cannot offer what the interface never asks for.
There is a test asserting the exact shape, so adding a fifth field is a deliberate act with a failing test attached rather than an autocomplete accident.
The allowlist is not a default either
egressAllowlist is required, and it is derived from the worker's owned-surface policy rather than passed by the caller — so a task cannot widen its own reach. Navigation outside it is refused before a browsing context is created, and every subresource is failed at request time:
case "Fetch.requestPaused": {
if (this.#hostAllowed(url)) await continueRequest(requestId);
else { this.#blocked.push(url); await failRequest(requestId, "BlockedByClient"); }
}
That second half matters more than it looks. Without it, rendering a page of ours that embeds a third-party script means our worker makes that third-party request. The allowlist would be true of navigation and false of everything the page pulls in.
The cost
This is genuinely restrictive. The worker cannot log into anything, cannot render a page behind auth, and cannot follow a link off our own domain. Several plausible features are now impossible without an explicit, visible change to a shared interface.
That is the trade. We decided the class of thing we want to be unable to do is worth more than the features we are giving up.
When this applies
Not everywhere. Most options should be options. This pattern earns its cost when:
- The capability would be a terms violation, a privacy problem, or a security incident rather than a bug
- The pressure to enable it will arrive later, under a deadline, from someone who wasn't in this conversation
- The blast radius is other people rather than you
If all three are true, delete the field.
Top comments (0)