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 in
mixFetchare periodically updated. If you get a certificate error, the root certificate you need might not be valid yet. Send a PR (opens in a new tab) if you need changes to the certificates. - If you are using
mixFetchin a web app with HTTPS, you will need to use a gateway that has Secure Websockets (WSS) to avoid a mixed content (opens in a new tab) error. mixFetchsupports concurrent requests (up to 10) to the same or different URLs.
Right now Gateways are not required to run a Secure Websocket (WSS) listener, so only a subset of nodes running in Gateway mode have configured their nodes to do so. You need to select a Gateway that has WSS from Harbourmaster (opens in a new tab).
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-fatConfiguration
import type { SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
const mixFetchOptions: SetupMixFetchOps = {
clientId: "docs-mixfetch-demo",
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
};Full Example
This example shows explicit initialization 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 }]);
};
// Initialize 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>
);
};