mixFetch
An easy way to secure parts or all of your web app is to replace calls to fetch (opens in a new tab) with mixFetch. It works the same as vanilla fetch: it's a proxied wrapper around the original function.
Things to be aware of:
- CA certificates are bundled into the WASM binary at build time. They're updated with each SDK release, so if you hit a certificate error, update to the latest
@nymproject/mix-fetch-full-fatversion. - HTTPS and WSS. When serving your app over HTTPS, the mixnet connection must also use Secure WebSockets to avoid a mixed content (opens in a new tab) error. Set
forceTls: truein yourSetupMixFetchOpsconfig (see below) and the SDK will automatically select a WSS-capable gateway. mixFetchsupports concurrent requests (up to 10) to the same or different URLs.
Environment Setup
Create a new project with Vite:
npm create vite@latestChoose React + TypeScript, then:
cd <YOUR_APP>
npm i
npm run devInstallation
npm install @nymproject/mix-fetch-full-fat @mui/material @emotion/react @emotion/styledThe MUI packages are used by the example UI below. If you only need mixFetch itself, install only @nymproject/mix-fetch-full-fat.
Configuration
import type { SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
const mixFetchOptions: SetupMixFetchOps = {
clientId: "my-app",
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // use Secure WebSockets (required when serving over HTTPS)
};preferredGateway is optional. If omitted, the SDK auto-selects a gateway. You can pin a specific one via Harbourmaster (opens in a new tab).
Full Example
This example shows explicit initialisation via createMixFetch, single URL fetch, and concurrent requests. Results appear both in the UI and in a visible log panel.
For this example we use the full-fat version of the ESM SDK. If you use the unbundled ESM variant, make sure your bundler configuration copies the WASM and web worker files to the output bundle.
import React, { useState, useRef, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat";
import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
const defaultUrl =
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
const args = { mode: "unsafe-ignore-cors" };
const mixFetchOptions: SetupMixFetchOps = {
clientId: "docs-mixfetch-demo",
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true,
};
// Log entry type for the visible log panel
type LogLevel = "info" | "error" | "send" | "receive";
type LogEntry = { timestamp: string; message: string; level: LogLevel };
const logColors: Record<LogLevel, string> = {
info: "gray",
error: "red",
send: "blue",
receive: "green",
};
const logLabels: Record<LogLevel, string> = {
info: "INFO",
error: "ERROR",
send: "SEND",
receive: "RECV",
};
export const MixFetch = () => {
// MixFetch initialization state
const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">("idle");
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// Log panel state
const [logs, setLogs] = useState<LogEntry[]>([]);
const logEndRef = useRef<HTMLDivElement>(null);
// Single fetch state
const [url, setUrl] = useState<string>(defaultUrl);
const [html, setHtml] = useState<string>();
const [busy, setBusy] = useState<boolean>(false);
// Concurrent fetch state
const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);
// Auto-scroll log panel to bottom
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [logs]);
// Helper to add a timestamped log entry
const addLog = (message: string, level: LogLevel) => {
const timestamp = new Date().toISOString().substring(11, 23);
setLogs((prev) => [...prev, { timestamp, message, level }]);
};
// Initialise MixFetch explicitly via createMixFetch
const handleStart = async () => {
try {
setStatus("starting");
setErrorMsg(null);
addLog("Starting MixFetch...", "info");
await createMixFetch(mixFetchOptions);
setStatus("ready");
addLog("MixFetch is ready!", "info");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setStatus("error");
setErrorMsg(msg);
addLog(`Error: ${msg}`, "error");
}
};
// Single URL fetch (reuses the existing MixFetch singleton)
const handleFetch = async () => {
try {
setBusy(true);
setHtml(undefined);
addLog(`Sending request to ${url}...`, "send");
const response = await mixFetch(url, args, mixFetchOptions);
const resHtml = await response.text();
setHtml(resHtml);
addLog(`Response received (${resHtml.length} bytes)`, "receive");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
addLog(`Fetch error: ${msg}`, "error");
} finally {
setBusy(false);
}
};
// Send 5 concurrent requests to different URLs on the same domain
const handleConcurrentFetch = async () => {
const baseUrl = "https://jsonplaceholder.typicode.com/posts/";
const count = 5;
try {
setConcurrentBusy(true);
setConcurrentResults([]);
addLog(
`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`,
"send",
);
const requests = Array.from({ length: count }, (_, i) => {
const targetUrl = `${baseUrl}${i + 1}`;
return mixFetch(targetUrl, args, mixFetchOptions)
.then((res) => res.json())
.then((json: { id: number; title: string }) => {
const entry = `[${json.id}] ${json.title}`;
addLog(entry, "receive");
return entry;
});
});
const results = await Promise.all(requests);
setConcurrentResults(results);
addLog(`All ${count} concurrent requests completed!`, "info");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
addLog(`Concurrent fetch error: ${msg}`, "error");
} finally {
setConcurrentBusy(false);
}
};
const isReady = status === "ready";
return (
<div style={{ marginTop: "1rem" }}>
{/* Start MixFetch */}
<Paper sx={{ p: 2, mb: 2 }} variant="outlined">
<Stack direction="row" alignItems="center" spacing={2}>
<Button
variant="contained"
disabled={status === "starting" || status === "ready"}
onClick={handleStart}
>
Start MixFetch
</Button>
{status === "starting" && <CircularProgress size={20} />}
<Typography fontFamily="monospace" fontSize="small">
{status === "idle" ? "Not started" :
status === "starting" ? "Starting..." :
status === "ready" ? "Ready" :
`Error: ${errorMsg}`}
</Typography>
</Stack>
</Paper>
{/* Fetch controls (disabled until MixFetch is ready) */}
<Box sx={{ opacity: isReady ? 1 : 0.5, pointerEvents: isReady ? "auto" : "none" }}>
{/* Single fetch */}
<Stack direction="row">
<TextField
disabled={busy}
fullWidth
label="URL"
type="text"
variant="outlined"
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
/>
<Button variant="outlined" disabled={busy} sx={{ marginLeft: "1rem" }} onClick={handleFetch}>
Fetch
</Button>
</Stack>
{busy && <Box mt={2}><CircularProgress /></Box>}
{html && (
<>
<Box mt={2}><strong>Response</strong></Box>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">{html}</Typography>
</Paper>
</>
)}
{/* Concurrent fetch */}
<Box mt={3}>
<strong>Concurrent Requests</strong>
<Box mt={1}>
<Button variant="outlined" disabled={concurrentBusy} onClick={handleConcurrentFetch}>
Send 5 Concurrent Requests (posts/1-5)
</Button>
</Box>
</Box>
{concurrentBusy && <Box mt={2}><CircularProgress /></Box>}
{concurrentResults.length > 0 && (
<Paper sx={{ p: 2, mt: 2 }} elevation={4}>
{concurrentResults.map((result, i) => (
<Typography key={i} fontFamily="monospace" fontSize="small">{result}</Typography>
))}
</Paper>
)}
</Box>
{/* Log Panel */}
{logs.length > 0 && (
<Paper sx={{ p: 2, mt: 3, maxHeight: 200, overflow: "auto" }} variant="outlined">
<strong>Log</strong>
{logs.map((entry, i) => (
<Typography key={i} fontFamily="monospace" fontSize="small" sx={{ color: logColors[entry.level] }}>
{entry.timestamp} [{logLabels[entry.level]}] {entry.message}
</Typography>
))}
<div ref={logEndRef} />
</Paper>
)}
</div>
);
};