opaque-fetch
v1.0.1
Published
Perform fetch requests from an opaque origin using a sandboxed iframe.
Maintainers
Readme
opaque-fetch
Perform fetch() requests from an opaque origin using a sandboxed iframe.
opaqueFetch() provides a fetch-like API while moving the actual request into a sandboxed browsing context with an opaque origin.
import { opaqueFetch } from 'opaque-fetch/index.js'
const response = await opaqueFetch('https://httpbin.org/get')
console.log(response.status)
console.log(await response.text())Why?
A normal fetch() is performed from the origin of the document that calls it.
https://my-site.example
│
│ fetch()
▼
https://httpbin.orgSometimes you want the request to originate from a separate, opaque origin instead.
opaqueFetch() use a sandboxed iframe without allow-same-origin:
<iframe sandbox="allow-scripts">The document inside the iframe therefore receives an opaque origin.
When serialized by the browser, this origin is represented as:
nullThe actual request becomes:
sandboxed iframe
origin: null
│
│ fetch()
▼
https://httpbin.orgHow it works
The implementation consists entirely of standard browser APIs.
- A hidden sandboxed iframe is created.
- The iframe runs a small ES module-compatible script through
srcdoc. - The iframe receives fetch parameters through
postMessage(). - The iframe performs the request using its own
fetch(). - The response body is transferred back through a
MessageChannel. opaqueFetch()reconstructs and returns a normalResponse.
No proxy server or external service is involved.
API
opaqueFetch(url, options?)
const response = await opaqueFetch(url, options)The API intentionally follows the standard Fetch API.
const response = await opaqueFetch(
'https://httpbin.org/post',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
hello: 'world'
})
}
)Return value
Returns a standard:
Promise<Response>For example:
const response = await opaqueFetch(url)
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`)
}
const data = await response.json()Request bodies
opaqueFetch() supports normal Fetch API request bodies as well as transferable streams and serialized browser types.
ReadableStream
Readable streams are transferred directly to the sandboxed iframe:
const response = await opaqueFetch(url, {
method: 'POST',
body: stream
})FormData and URLSearchParams
FormData and URLSearchParams is serialized before crossing the iframe boundary and reconstructed inside the iframe:
const form = new FormData()
form.append('name', 'Alice')
form.append('message', 'Hello')
const response = await opaqueFetch(url, {
method: 'POST',
body: form
})Response streaming
The response body is transferred from the iframe instead of being fully buffered first.
This allows the returned Response to retain a streaming body:
const response = await opaqueFetch(url)
for await (const chunk of response.body) {
// Process Uint8Array chunk
console.log(chunk)
}Normal Response methods are available:
await response.text()
await response.json()
await response.arrayBuffer()
await response.blob()Origin
The sandbox intentionally omits allow-same-origin:
<iframe sandbox="allow-scripts">This causes the iframe document to have an opaque origin.
The browser serializes this origin as:
nullConsequently, requests made from the iframe are not associated with the origin of the parent document.
This is fundamentally different from attempting to set an Origin header manually. Origin is a browser-controlled request header and cannot simply be overridden by application JavaScript.
Referrer
Because the request originates from the sandboxed document rather than the parent document, the parent page's URL is not used as the request's normal document referrer.
This makes opaqueFetch() useful when the request should be decoupled from the page that initiated it.
CORS
The request still follows the browser's normal security model.
An opaque origin does not bypass CORS.
For example, a server may receive:
Origin: nulland can explicitly allow that origin:
Access-Control-Allow-Origin: nullIf the server does not permit the request, the browser will still enforce CORS.
Credentials
Credentials follow the normal Fetch API rules.
If credentials should not be included, specify:
const response = await opaqueFetch(url, {
credentials: 'omit'
})The opaque origin itself should not be treated as a replacement for explicit credential configuration.
No dependencies
The package has no runtime dependencies.
It does not depend on:
- Node.js APIs
- Deno APIs
- Bun APIs
- third-party libraries
- polyfills
- proxy servers
- external services
It uses standard browser APIs only.
Browser APIs
The implementation relies on standard Web APIs including:
fetch()ResponseHeadersFormDataURLSearchParamsReadableStreamMessageChannelpostMessage()HTMLIFrameElement- sandboxed iframes
Design goals
- Native — built entirely on browser APIs.
- Dependency-free — no runtime dependencies.
- Fetch-compatible — familiar
fetch()-style API. - Streaming — request and response streams can be transferred.
- Isolated — requests execute in a separate opaque-origin browsing context.
- Small — the implementation is intentionally minimal.
- Browser-first — no assumptions about Node.js, Deno, Bun, or another JavaScript runtime.
Limitations
opaqueFetch() is specifically an origin isolation primitive.
It does not provide a network proxy, VPN, Tor connection, or IP address masking. The request still travels directly from the browser to the destination server.
The browser's normal security policies still apply, including CORS, CSP, cookie policies, and other Fetch API restrictions.
License
MIT
