7c890ea0c5
* First sweep packages + some minor tweaking * Second sweep * Regenerate lockfile + package.json mods * Regenerate lockfile again * Fix CI * Fix CI again * All building properly * unblock * Tweak examples * Comments + readme + fix rotten unit test * First pass docs * Big pass * Massive pass on new docs * Update integrations.md w mobile * Partial overhaul review * new playground + big pass * new fix lychee err * IPR notice tweak
111 lines
5.8 KiB
Plaintext
111 lines
5.8 KiB
Plaintext
---
|
|
title: "mix-fetch guides"
|
|
description: "Request shape, default headers, drop-in caveats, and tunnel configuration for mix-fetch."
|
|
schemaType: "TechArticle"
|
|
section: "Developers"
|
|
lastUpdated: "2026-06-05"
|
|
---
|
|
|
|
import { Callout } from 'nextra/components'
|
|
|
|
# Reference
|
|
|
|
## Request shape
|
|
|
|
The `init` argument is the standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). Headers, method, and body all work. `AbortController` (`signal`) is not supported: an in-flight request cannot be cancelled.
|
|
|
|
```ts
|
|
const res = await mixFetch('https://httpbin.org/post', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ hello: 'mixnet' }),
|
|
});
|
|
console.log(await res.json());
|
|
```
|
|
|
|
Binary responses come back via the standard `Response.arrayBuffer()` / `Response.blob()` methods:
|
|
|
|
```ts
|
|
const res = await mixFetch('https://example.com/image.png');
|
|
const blob = await res.blob();
|
|
```
|
|
|
|
Repeated headers (`Set-Cookie`, `Vary`, `Link`, `WWW-Authenticate`) are preserved. The wasm side returns headers as a `[name, value]` pair sequence, which `Headers` reconstructs verbatim.
|
|
|
|
## Default request headers
|
|
|
|
When the caller doesn't set them, `mixFetch` injects four browser-shape headers before the request leaves the tunnel. The shim exists because many CDNs (cloudflare's bot management) and host policies (wikimedia's User-Agent policy) reject requests that look unlike a real browser. Caller-supplied values always win.
|
|
|
|
| Header | Injected default |
|
|
|--------|------------------|
|
|
| `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` |
|
|
| `Accept` | `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8` |
|
|
| `Accept-Language` | `en-US,en;q=0.9` |
|
|
| `Accept-Encoding` | `identity` |
|
|
|
|
`Accept-Encoding` is forced to `identity` rather than `gzip, deflate, br` because the wasm build has no decompressor. Advertising compression would let the server return a compressed body the wasm build cannot decode, so `Response.text()` or `.json()` would see raw compressed bytes. Responses therefore arrive uncompressed, so large text or JSON bodies transfer more bytes over the slower mixnet path.
|
|
|
|
To override any of these, set the header in the `init.headers` bag like normal:
|
|
|
|
```ts
|
|
const res = await mixFetch('https://example.com', {
|
|
headers: { 'User-Agent': 'my-app/1.0' },
|
|
});
|
|
```
|
|
|
|
<Callout type="info">
|
|
The shim does not attempt full browser impersonation. TLS fingerprint (JA3), HTTP/2, and header ordering are still distinguishable from a real Chrome request. If you need stronger blend-in, you'll need to handle that at the application or destination layer.
|
|
</Callout>
|
|
|
|
## Drop-in caveats
|
|
|
|
`mixFetch` matches the `fetch()` call signature but is not a perfect substitute. The differences are intentional and follow from running outside the browser's networking stack:
|
|
|
|
| Difference | What it means | What to do |
|
|
|---|---|---|
|
|
| **No same-origin restriction** | Requests aren't subject to browser CORS preflight. The IPR honours its exit policy regardless of `Origin`. | Don't rely on CORS as an access-control mechanism for `mixFetch` requests; treat them as you would server-to-server calls. |
|
|
| **No cookies / credentials** | The browser cookie jar is not shared with the WASM instance. `credentials: 'include'` has no effect. | Pass auth tokens via `Authorization` or other explicit headers. |
|
|
| **No HTTP cache** | The browser HTTP cache is not consulted. Every call hits the network. | Cache responses at the application layer if needed. |
|
|
| **No service-worker interception** | Requests don't pass through any `fetch` event handlers registered by service workers. | n/a |
|
|
| **HTTPS only in practice** | The IPR sees plaintext HTTP in full. | Always target `https://` URLs. |
|
|
|
|
## Errors
|
|
|
|
`mixFetch` follows `fetch` semantics for HTTP status: a 4xx or 5xx response **resolves** with a `Response` carrying that status, so check `response.ok` or `response.status` yourself. The promise **rejects** only on a transport-level failure: a connection or TLS failure, a DNS failure, or the IPR refusing the destination under its exit policy. A rejection is a plain `Error` whose message describes the cause; there is no typed error class, so match on the message if you need to branch.
|
|
|
|
## Timeouts and cancellation
|
|
|
|
There is no per-request timeout, and `AbortController` / `signal` is ignored: an in-flight `mixFetch` cannot be cancelled. To bound how long you wait, race it against a timer. This stops you waiting but does not cancel the underlying request:
|
|
|
|
```ts
|
|
const res = await Promise.race([
|
|
mixFetch(url),
|
|
new Promise<Response>((_, reject) =>
|
|
setTimeout(() => reject(new Error('mixFetch timeout')), 30_000),
|
|
),
|
|
]);
|
|
```
|
|
|
|
Connection and DNS timeouts at the tunnel level are set once via `connectTimeoutMs` and `dnsTimeoutMs` in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
|
|
|
## Configuration
|
|
|
|
`setupMixTunnel(opts)` (and `createMixFetch(opts)`) accept the shared tunnel options from [`@nymproject/mix-tunnel`](/developers/mix-tunnel/guides#configuration). The most commonly touched are:
|
|
|
|
```ts
|
|
await setupMixTunnel({
|
|
// Pin a specific IPR (otherwise auto-discovered from the topology).
|
|
preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
|
|
|
// Lower latency and bandwidth at the cost of traffic-analysis resistance.
|
|
disableCoverTraffic: true,
|
|
disablePoissonTraffic: true,
|
|
});
|
|
```
|
|
|
|
The full option surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
|
|
|
<Callout type="info">
|
|
The first `mixFetch` call after `setupMixTunnel()` may take a few seconds: gateway handshake, IPR discovery, and the first DNS resolution all happen on demand. Subsequent calls reuse the tunnel and complete in roughly the time of a normal HTTPS request plus mixnet latency.
|
|
</Callout>
|