@ricsam/isolate-fetch
v0.1.15
Published
Fetch API implementation for isolated-vm V8 sandbox
Maintainers
Readme
@ricsam/isolate-fetch
Fetch API and HTTP server handler for isolated-vm V8 sandbox.
Installation
npm add @ricsam/isolate-fetchUsage
import { setupFetch } from "@ricsam/isolate-fetch";
const handle = await setupFetch(context, {
onFetch: async (url, init) => {
// Handle outbound fetch() calls from the isolate
console.log(`Fetching: ${url}`);
return fetch(url, init);
},
});Injected Globals
fetch,Request,Response,HeadersFormData,AbortController,AbortSignalWebSocket(WHATWG WebSocket client)serve(HTTP server handler)
Usage in Isolate
// Outbound fetch
const response = await fetch("https://api.example.com/data");
const data = await response.json();
// Request/Response
const request = new Request("https://example.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
const response = new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
// Static methods
Response.json({ message: "hello" });
Response.redirect("https://example.com", 302);
// AbortController
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await fetch(url, { signal: controller.signal });
// FormData
const formData = new FormData();
formData.append("name", "John");
formData.append("file", new File(["content"], "file.txt"));HTTP Server (serve)
The serve() function registers a request handler in the isolate that can receive HTTP requests dispatched from the host. It uses a Bun-compatible API.
Basic Usage
// In isolate code
serve({
fetch(request, server) {
const url = new URL(request.url);
return Response.json({ path: url.pathname, method: request.method });
}
});The fetch Handler
The fetch handler receives two arguments:
request- A standardRequestobjectserver- A server object with WebSocket upgrade capability
serve({
fetch(request, server) {
// Access request properties
const url = new URL(request.url);
const method = request.method;
const headers = request.headers;
const body = await request.json(); // for POST/PUT
// Return a Response
return new Response("Hello World");
}
});The server Object
The server argument provides WebSocket upgrade functionality:
serve({
fetch(request, server) {
// Check for WebSocket upgrade request
if (request.headers.get("Upgrade") === "websocket") {
// Upgrade the connection, optionally passing data
server.upgrade(request, { data: { userId: "123" } });
return new Response(null, { status: 101 });
}
return new Response("Not a WebSocket request", { status: 400 });
}
});WebSocket Support
The serve() function supports WebSocket connections through a websocket handler object.
WebSocket Handlers
serve({
fetch(request, server) {
if (request.headers.get("Upgrade") === "websocket") {
server.upgrade(request, { data: { userId: "123" } });
return new Response(null, { status: 101 });
}
return new Response("OK");
},
websocket: {
open(ws) {
// Called when a WebSocket connection is opened
console.log("Connected:", ws.data.userId);
ws.send("Welcome!");
},
message(ws, message) {
// Called when a message is received from the client
console.log("Received:", message);
ws.send("Echo: " + message);
},
close(ws, code, reason) {
// Called when the connection is closed
console.log("Closed:", code, reason);
},
error(ws, error) {
// Called when an error occurs
console.error("Error:", error);
}
}
});The ws Object
Each WebSocket handler receives a ws object with the following properties and methods:
| Property/Method | Description |
|-----------------|-------------|
| ws.data | Custom data passed during server.upgrade() |
| ws.send(message) | Send a message to the client (string or ArrayBuffer) |
| ws.close(code?, reason?) | Close the connection with optional code and reason |
| ws.readyState | Current state: 1 (OPEN), 2 (CLOSING), 3 (CLOSED) |
Optional Handlers
All WebSocket handlers are optional. You can define only the handlers you need:
// Only handle messages - no open/close/error handlers needed
serve({
fetch(request, server) { /* ... */ },
websocket: {
message(ws, message) {
ws.send("Echo: " + message);
}
}
});
// Only handle open and close
serve({
fetch(request, server) { /* ... */ },
websocket: {
open(ws) {
console.log("Connected");
},
close(ws, code, reason) {
console.log("Disconnected");
}
}
});WebSocket Client (Outbound Connections)
The isolate has access to a WHATWG-compliant WebSocket class for making outbound WebSocket connections. This is separate from the serve() WebSocket handler (which handles inbound connections).
Basic Usage
// In isolate code
const ws = new WebSocket("wss://api.example.com/stream");
ws.onopen = () => {
console.log("Connected!");
ws.send("Hello server");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onerror = (event) => {
console.error("WebSocket error");
};
ws.onclose = (event) => {
console.log("Closed:", event.code, event.reason);
};WHATWG WebSocket API
The WebSocket class implements the full WHATWG WebSocket specification:
Constructor:
new WebSocket(url)
new WebSocket(url, protocols)
new WebSocket(url, protocol) // Single protocol as stringProperties:
| Property | Type | Description |
|----------|------|-------------|
| url | string | The URL the WebSocket is connected to |
| readyState | number | Connection state (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED) |
| bufferedAmount | number | Bytes queued for transmission |
| protocol | string | Server-selected subprotocol |
| extensions | string | Server-selected extensions |
| binaryType | string | Binary data type: "blob" (default) or "arraybuffer" |
Methods:
| Method | Description |
|--------|-------------|
| send(data) | Send data (string, ArrayBuffer, Uint8Array, Blob) |
| close(code?, reason?) | Close the connection |
Events:
| Event | Type | Description |
|-------|------|-------------|
| open | Event | Connection established |
| message | MessageEvent | Message received (event.data contains the message) |
| error | Event | Error occurred |
| close | CloseEvent | Connection closed (event.code, event.reason, event.wasClean) |
EventTarget Interface:
ws.addEventListener("message", handler);
ws.removeEventListener("message", handler);Binary Data
const ws = new WebSocket("wss://api.example.com/binary");
// Set binaryType before receiving messages
ws.binaryType = "arraybuffer"; // or "blob" (default)
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(event.data);
console.log("Received binary:", bytes.length, "bytes");
}
};
// Send binary data
ws.onopen = () => {
const data = new Uint8Array([1, 2, 3, 4]);
ws.send(data);
};Protocol Negotiation
// Request specific subprotocols
const ws = new WebSocket("wss://api.example.com", ["graphql-ws", "subscriptions-transport-ws"]);
ws.onopen = () => {
// Check which protocol the server selected
console.log("Using protocol:", ws.protocol);
};Host-Side Control
The host can control outbound WebSocket connections via the webSocket callback:
// When creating a runtime (via @ricsam/isolate-client)
const runtime = await client.createRuntime({
webSocket: async (url, protocols) => {
// Block certain hosts
if (url.includes("blocked.com")) {
return null;
}
// Allow connection
return new WebSocket(url, protocols.length > 0 ? protocols : undefined);
},
});See the @ricsam/isolate-client README for more details on the WebSocket callback.
Host-Side API
The host dispatches requests and WebSocket events to the isolate.
Dispatching HTTP Requests
// From host code
const response = await handle.dispatchRequest(
new Request("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Alice" }),
})
);
console.log(await response.json());WebSocket Flow
The host manages WebSocket connections and dispatches events to the isolate:
// 1. Dispatch the upgrade request
await handle.dispatchRequest(
new Request("http://localhost/ws", {
headers: { "Upgrade": "websocket" }
})
);
// 2. Check if isolate requested an upgrade
const upgradeRequest = handle.getUpgradeRequest();
if (upgradeRequest?.requested) {
const connectionId = upgradeRequest.connectionId;
// 3. Register callback for commands FROM the isolate
handle.onWebSocketCommand((cmd) => {
if (cmd.type === "message") {
// Isolate called ws.send() - forward to real WebSocket
realWebSocket.send(cmd.data);
} else if (cmd.type === "close") {
// Isolate called ws.close()
realWebSocket.close(cmd.code, cmd.reason);
}
});
// 4. Notify isolate the connection is open (triggers websocket.open)
handle.dispatchWebSocketOpen(connectionId);
// 5. Forward messages TO the isolate (triggers websocket.message)
realWebSocket.onmessage = (event) => {
handle.dispatchWebSocketMessage(connectionId, event.data);
};
// 6. Forward close events TO the isolate (triggers websocket.close)
realWebSocket.onclose = (event) => {
handle.dispatchWebSocketClose(connectionId, event.code, event.reason);
};
}Host API Reference
| Method | Description |
|--------|-------------|
| dispatchRequest(request) | Dispatch HTTP request to isolate's serve() handler |
| hasServeHandler() | Check if serve() has been called in isolate |
| hasActiveConnections() | Check if there are active WebSocket connections |
| getUpgradeRequest() | Get pending WebSocket upgrade request info |
| dispatchWebSocketOpen(id) | Notify isolate that WebSocket connection opened |
| dispatchWebSocketMessage(id, data) | Send message to isolate's websocket.message handler |
| dispatchWebSocketClose(id, code, reason) | Notify isolate that connection closed |
| dispatchWebSocketError(id, error) | Notify isolate of WebSocket error |
| onWebSocketCommand(callback) | Register callback for ws.send()/ws.close() from isolate |
WebSocket Command Types
Commands received via onWebSocketCommand:
interface WebSocketCommand {
type: "message" | "close";
connectionId: string;
data?: string | ArrayBuffer; // For "message" type
code?: number; // For "close" type
reason?: string; // For "close" type
}Complete Example
// Host code
import { setupFetch } from "@ricsam/isolate-fetch";
const handle = await setupFetch(context, {
onFetch: async (url, init) => fetch(url, init),
});
// Set up serve handler in isolate
await context.eval(`
serve({
fetch(request, server) {
const url = new URL(request.url);
// WebSocket upgrade
if (url.pathname === "/ws" && request.headers.get("Upgrade") === "websocket") {
server.upgrade(request, { data: { path: url.pathname } });
return new Response(null, { status: 101 });
}
// Regular HTTP
return Response.json({ path: url.pathname });
},
websocket: {
open(ws) {
console.log("WebSocket connected to:", ws.data.path);
ws.send("Connected!");
},
message(ws, message) {
console.log("Received:", message);
ws.send("Echo: " + message);
},
close(ws, code, reason) {
console.log("Closed:", code, reason);
}
}
});
`, { promise: true });
// Dispatch HTTP request
const response = await handle.dispatchRequest(
new Request("http://localhost/api/users")
);
console.log(await response.json()); // { path: "/api/users" }
// Handle WebSocket connection
await handle.dispatchRequest(
new Request("http://localhost/ws", { headers: { "Upgrade": "websocket" } })
);
const upgrade = handle.getUpgradeRequest();
if (upgrade?.requested) {
// Listen for commands from isolate
handle.onWebSocketCommand((cmd) => {
console.log("Command from isolate:", cmd);
});
// Open connection (triggers websocket.open)
handle.dispatchWebSocketOpen(upgrade.connectionId);
// Send message (triggers websocket.message)
handle.dispatchWebSocketMessage(upgrade.connectionId, "Hello!");
}License
MIT
