The @gcoredev/fastedge-test debugger server exposes a REST API for loading WASM modules, executing requests, and managing test configuration.
Note on header values. Response-side and hook-result headers use
Record<string, string | string[]>— single-valued headers are astring, multi-valued headers (notablySet-Cookieper RFC 6265) are astring[]. Request-side header inputs are single-valuedRecord<string, string>.
http://localhost:5179
The port can be overridden via the PORT environment variable. The active port is written to .fastedge-debug/.debug-port (relative to WORKSPACE_PATH if set, otherwise the current working directory) on startup and deleted on shutdown. The file contains PORT:SHA256_HASH, where PORT is the decimal port number and SHA256_HASH is the hex-encoded SHA-256 of the session token. Consumers that only need the port should parse the prefix up to the first colon (e.g. parseInt(content.split(":")[0], 10)).
The debugger server requires a session token on all /api/* requests. /health is the only unauthenticated endpoint.
When started from the CLI (npx fastedge-debug), the server generates a random 32-byte hex token and prints the full browser URL to stderr:
Open: http://localhost:5179/#token=<hex>
The fragment after #token= is your session token. Copy it for use in API calls or WebSocket connections.
HTTP requests — send the token as a header on every /api/* call:
x-fastedge-token: <token>
WebSocket — two mechanisms exist, tried in preference order:
Sec-WebSocket-Protocol: fastedge-token.<token>(preferred) — the token is embedded in the subprotocol list rather than the URL, keeping it out of proxy and server access logs. The server echoes the subprotocol back on accept.?token=<token>query parameter (fallback) — for legacy or non-browser tooling that cannot set subprotocols on the WebSocket handshake. Avoid this form in new consumers: the token appears in server and proxy logs.
const ws = new WebSocket(
`ws://127.0.0.1:5179/ws`,
[`fastedge-token.${token}`],
);ws://127.0.0.1:<port>/ws?token=<token>
Environment variables related to authentication and binding:
| Variable | Default | Description |
|---|---|---|
FASTEDGE_DEBUG_TOKEN |
unset | Inject a known token instead of generating one. When set, the Open: URL is not printed to stderr (the VSCode extension uses this path). |
FASTEDGE_BIND_HOST |
127.0.0.1 |
Interface the HTTP server binds to. |
FASTEDGE_EXPECTED_HOST |
unset | Extra hostname (suffix match) allowed in Host / Origin headers — for Codespaces forwarded URLs. |
WORKSPACE_PATH |
process.cwd() |
Workspace root; affects .env resolution, port file, and config file placement. |
Open question — token file for local tooling: Should the server also write the session token to
.fastedge-debug/.debug-token(mode0600, same trust boundary as.env) alongside.fastedge-debug/.debug-port, so local tooling can find the token without parsing stderr? Decision pending from repo owner — document the answer here when made. If yes, implement inwritePortFileand update this section; if no, note that tooling must capture theOpen:stderr line.
The POST /api/execute, POST /api/send, and POST /api/config endpoints accept an optional X-Source request header that tags the origin of the operation in WebSocket broadcast events.
| Value | Description |
|---|---|
ui |
Request originated from the web UI (default if omitted) |
ai_agent |
Request originated from an AI agent |
api |
Request originated from direct API usage |
system |
Request originated from an automated system |
X-Source: ai_agentReturns the server status and service identity.
Response
{
status: "ok";
service: "fastedge-debugger";
}Example
curl http://localhost:5179/health{
"status": "ok",
"service": "fastedge-debugger"
}Returns the number of currently connected WebSocket clients. Useful in CI tooling to wait until the UI has connected before proceeding.
Response
{
count: number;
}Example
curl -H "x-fastedge-token: <token>" http://localhost:5179/api/client-count{
"count": 1
}Loads a WASM binary into the runner. Accepts either a file path or a base64-encoded binary. Automatically detects whether the module is HTTP-WASM or Proxy-WASM.
Request Body
Exactly one of wasmPath or wasmBase64 must be provided; providing both is an error.
{
wasmPath?: string; // Absolute path to a .wasm file on the server filesystem
wasmBase64?: string; // Base64-encoded WASM binary; mutually exclusive with wasmPath
dotenv?: {
enabled?: boolean; // Whether to load .env files for this module
path?: string; // Directory containing .env files (defaults to server CWD)
};
httpPort?: number; // HTTP-WASM only. Pin the runner subprocess to this port (1024–65535).
// Load fails immediately if the port is already in use.
// Ignored for proxy-wasm modules.
}Response
{
ok: true;
wasmType: "http-wasm" | "proxy-wasm";
resolvedPath?: string; // Absolute path used when wasmPath was provided
}Example — load from path
curl -X POST http://localhost:5179/api/load \
-H "Content-Type: application/json" \
-d '{
"wasmPath": "/home/user/project/build/module.wasm",
"dotenv": { "enabled": true }
}'{
"ok": true,
"wasmType": "proxy-wasm",
"resolvedPath": "/home/user/project/build/module.wasm"
}Example — load from base64
curl -X POST http://localhost:5179/api/load \
-H "Content-Type: application/json" \
-d '{
"wasmBase64": "AGFzbQEAAAA...",
"dotenv": { "enabled": false }
}'{
"ok": true,
"wasmType": "http-wasm"
}Example — pin HTTP-WASM to a specific port
curl -X POST http://localhost:5179/api/load \
-H "Content-Type: application/json" \
-d '{
"wasmPath": "/home/user/project/build/app.wasm",
"httpPort": 8100
}'{
"ok": true,
"wasmType": "http-wasm",
"resolvedPath": "/home/user/project/build/app.wasm"
}Error Responses
| Status | Condition |
|---|---|
400 |
Validation failed, missing both wasmPath and wasmBase64, invalid path, or path does not end in .wasm |
400 |
httpPort is specified and already in use (HTTP-WASM only) |
500 |
WASM load failed or runner initialization error |
Applies updated dotenv settings to the currently loaded WASM module without reloading the binary. For Proxy-WASM, this resets stores and reloads dotenv files in-place. For HTTP-WASM, this restarts the underlying process with updated flags.
Requires a WASM module to already be loaded via POST /api/load.
Request Body
{
dotenv: {
enabled: boolean; // Whether dotenv loading should be enabled
path?: string; // Directory containing .env files (defaults to server CWD)
};
}Response
{
ok: true;
}Example
curl -X PATCH http://localhost:5179/api/dotenv \
-H "Content-Type: application/json" \
-d '{
"dotenv": { "enabled": true, "path": "/home/user/project" }
}'{
"ok": true
}Error Responses
| Status | Condition |
|---|---|
400 |
dotenv.enabled is not a boolean, or no WASM module is loaded |
500 |
Failed to apply dotenv settings |
Executes a request through the loaded WASM module. Behavior differs based on the detected runner type. This endpoint does not use schema validation — fields are read directly from the request body.
Requires a WASM module to be loaded via POST /api/load. Accepts an optional X-Source request header.
Request Body
For HTTP-WASM, provide either path (preferred) or url (legacy). When path is given, it is used directly as the request path (e.g. /api/hello?q=1). When only url is given, the path and query string are extracted from it.
{
path?: string; // Request path and query string (preferred)
url?: string; // Full URL — path and query extracted (legacy fallback)
method?: string; // HTTP method (default: "GET")
headers?: Record<string, string>; // Request headers (default: {})
body?: string; // Request body (default: "")
}For Proxy-WASM, the top-level url field is required. The full CDN flow is controlled via nested request and properties fields. The upstream response is generated at runtime — either by a real fetch against url, or by the built-in responder when url === "built-in":
{
url: string; // Request URL, or "built-in" (required)
request?: {
method?: string; // HTTP method (default: "GET")
headers?: Record<string, string>; // Request headers (default: {})
body?: string; // Request body (default: "")
};
properties?: Record<string, unknown>; // CDN properties (default: {})
}Response — HTTP-WASM
{
ok: true;
result: {
status: number;
statusText: string;
headers: Record<string, string | string[]>;
body: string;
contentType: string | null;
isBase64?: boolean;
logs: Array<{ level: number; message: string }>;
};
}Response — Proxy-WASM
{
ok: true;
hookResults: Record<string, HookResult>;
finalResponse: {
status: number;
statusText: string;
headers: Record<string, string | string[]>;
body: string;
contentType: string;
isBase64?: boolean;
};
calculatedProperties?: Record<string, unknown>;
}Where HookResult is:
type HookResult = {
returnCode: number | null;
logs: Array<{ level: number; message: string }>;
input: {
request: { headers: Record<string, string | string[]>; body: string };
response: { headers: Record<string, string | string[]>; body: string };
properties?: Record<string, unknown>;
};
output: {
request: { headers: Record<string, string | string[]>; body: string };
response: { headers: Record<string, string | string[]>; body: string };
properties?: Record<string, unknown>;
};
properties: Record<string, unknown>;
};hookResults is keyed by hook name (e.g. "onRequestHeaders", "onResponseHeaders"). calculatedProperties is present only when the runner derives request-derived properties; read PropertyResolver.getCalculatedProperties() in the runner source for the current list of derived request.* keys.
Example — HTTP-WASM
curl -X POST http://localhost:5179/api/execute \
-H "Content-Type: application/json" \
-H "X-Source: api" \
-d '{
"path": "/api/data?format=json",
"method": "GET",
"headers": { "accept": "application/json" }
}'{
"ok": true,
"result": {
"status": 200,
"statusText": "OK",
"headers": { "content-type": "application/json" },
"body": "{\"hello\":\"world\"}",
"contentType": "application/json",
"isBase64": false,
"logs": [
{ "level": 2, "message": "request received" }
]
}
}Example — Proxy-WASM
curl -X POST http://localhost:5179/api/execute \
-H "Content-Type: application/json" \
-H "X-Source: api" \
-d '{
"url": "https://example.com/page",
"request": {
"method": "GET",
"headers": { "host": "example.com" },
"body": ""
},
"properties": {}
}'{
"ok": true,
"hookResults": {
"onRequestHeaders": {
"returnCode": 0,
"logs": [{ "level": 2, "message": "onRequestHeaders called" }],
"input": {
"request": { "headers": { "host": "example.com" }, "body": "" },
"response": { "headers": {}, "body": "" },
"properties": {}
},
"output": {
"request": { "headers": { "host": "example.com", "x-added": "1" }, "body": "" },
"response": { "headers": {}, "body": "" }
},
"properties": {}
}
},
"finalResponse": {
"status": 200,
"statusText": "OK",
"headers": { "content-type": "text/html" },
"body": "<html/>",
"contentType": "text/html"
},
"calculatedProperties": {
"request.url": "https://example.com/page",
"request.host": "example.com",
"request.path": "/page"
}
}Error Responses
| Status | Condition |
|---|---|
400 |
No WASM module loaded, or missing path/url for HTTP-WASM, or missing url for Proxy-WASM |
500 |
Execution failed |
Calls a specific Proxy-WASM CDN hook directly. Only valid for Proxy-WASM modules.
Requires a WASM module to be loaded via POST /api/load.
Request Body
{
hook: "onRequestHeaders" | "onRequestBody" | "onResponseHeaders" | "onResponseBody";
request?: {
headers: Record<string, string>;
body: string;
};
response?: {
headers: Record<string, string>;
body: string;
};
properties: Record<string, unknown>; // Required; use {} if none
}request and response default to { headers: {}, body: "" } if omitted.
Response
{
ok: true;
result: HookResult;
}Where HookResult is:
type HookResult = {
returnCode: number | null;
logs: Array<{ level: number; message: string }>;
input: {
request: { headers: Record<string, string | string[]>; body: string };
response: { headers: Record<string, string | string[]>; body: string };
properties?: Record<string, unknown>;
};
output: {
request: { headers: Record<string, string | string[]>; body: string };
response: { headers: Record<string, string | string[]>; body: string };
properties?: Record<string, unknown>;
};
properties: Record<string, unknown>;
};Example
curl -X POST http://localhost:5179/api/call \
-H "Content-Type: application/json" \
-d '{
"hook": "onRequestHeaders",
"request": {
"headers": { "host": "example.com", "user-agent": "curl/8.0" },
"body": ""
},
"response": {
"headers": {},
"body": ""
},
"properties": {
"client.geo.country": "US"
}
}'{
"ok": true,
"result": {
"returnCode": 0,
"logs": [
{ "level": 2, "message": "processing request headers" }
],
"input": {
"request": {
"headers": { "host": "example.com", "user-agent": "curl/8.0" },
"body": ""
},
"response": { "headers": {}, "body": "" },
"properties": { "client.geo.country": "US" }
},
"output": {
"request": {
"headers": { "host": "example.com", "user-agent": "curl/8.0", "x-country": "US" },
"body": ""
},
"response": { "headers": {}, "body": "" }
},
"properties": { "client.geo.country": "US" }
}
}Error Responses
| Status | Condition |
|---|---|
400 |
Validation failed (invalid hook name, missing properties), or no WASM module loaded |
500 |
Hook execution failed |
Executes the full Proxy-WASM CDN request/response flow. Equivalent to POST /api/execute for Proxy-WASM, but uses stricter Zod schema validation on the request body. Only valid for Proxy-WASM modules.
Requires a WASM module to be loaded via POST /api/load. Accepts an optional X-Source request header.
Request Body
{
url: string | "built-in"; // Full request URL, or "built-in" to use the built-in responder
request?: {
method?: string; // HTTP method (default: "GET")
url?: string;
headers?: Record<string, string>; // Request headers (default: {})
body?: string; // Request body (default: "")
};
properties: Record<string, unknown>; // CDN properties (required; use {} if none)
}The upstream response is generated at runtime — either by a real fetch against url, or by the built-in responder when url === "built-in".
Response
{
ok: true;
hookResults: Record<string, HookResult>;
finalResponse: {
status: number;
statusText: string;
headers: Record<string, string | string[]>;
body: string;
contentType: string;
isBase64?: boolean;
};
calculatedProperties?: Record<string, unknown>;
}HookResult has the same shape as documented in POST /api/call. hookResults is keyed by hook name. calculatedProperties is optional; keys follow the request.* pattern derived by PropertyResolver.getCalculatedProperties().
Example
curl -X POST http://localhost:5179/api/send \
-H "Content-Type: application/json" \
-H "X-Source: ai_agent" \
-d '{
"url": "https://example.com/api/resource",
"request": {
"method": "POST",
"headers": { "content-type": "application/json" },
"body": "{\"key\":\"value\"}"
},
"properties": {
"client.geo.country": "DE"
}
}'{
"ok": true,
"hookResults": {
"onRequestHeaders": {
"returnCode": 0,
"logs": [],
"input": {
"request": { "headers": { "content-type": "application/json" }, "body": "" },
"response": { "headers": {}, "body": "" },
"properties": { "client.geo.country": "DE" }
},
"output": {
"request": { "headers": { "content-type": "application/json" }, "body": "" },
"response": { "headers": {}, "body": "" }
},
"properties": { "client.geo.country": "DE" }
},
"onResponseHeaders": {
"returnCode": 0,
"logs": [],
"input": {
"request": { "headers": { "content-type": "application/json" }, "body": "" },
"response": { "headers": { "content-type": "application/json" }, "body": "" },
"properties": { "client.geo.country": "DE" }
},
"output": {
"request": { "headers": { "content-type": "application/json" }, "body": "" },
"response": { "headers": { "content-type": "application/json" }, "body": "" }
},
"properties": { "client.geo.country": "DE" }
}
},
"finalResponse": {
"status": 200,
"statusText": "OK",
"headers": { "content-type": "application/json" },
"body": "{\"result\":\"ok\"}",
"contentType": "application/json"
},
"calculatedProperties": {
"request.url": "https://example.com/api/resource",
"request.host": "example.com",
"request.path": "/api/resource"
}
}Error Responses
| Status | Condition |
|---|---|
400 |
Validation failed (missing url or properties), or no WASM module loaded |
500 |
Execution failed |
Reads fastedge-config.test.json from the .fastedge-debug/ directory (relative to WORKSPACE_PATH if set, otherwise the current working directory) and returns it along with a validation result.
Response
{
ok: true;
config: TestConfig;
valid: boolean;
validationErrors?: {
formErrors: string[];
fieldErrors: Record<string, string[]>;
};
}TestConfig is a discriminated union on appType:
// Proxy-WASM config (appType defaults to "proxy-wasm")
type ProxyWasmConfig = {
$schema?: string;
description?: string;
appType: "proxy-wasm";
wasm?: { path: string; description?: string };
request: {
method: string;
url: string;
headers: Record<string, string>;
body: string;
};
properties: Record<string, unknown>;
dotenv?: { enabled?: boolean; path?: string };
};
// HTTP-WASM config
type HttpWasmConfig = {
$schema?: string;
description?: string;
appType: "http-wasm";
wasm?: { path: string; description?: string };
httpPort?: number; // Pin the runner subprocess to this port (1024–65535)
request: {
method: string;
path: string;
headers: Record<string, string>;
body: string;
};
properties: Record<string, unknown>;
dotenv?: { enabled?: boolean; path?: string };
};
type TestConfig = ProxyWasmConfig | HttpWasmConfig;Example
curl http://localhost:5179/api/config{
"ok": true,
"config": {
"$schema": "http://localhost:5179/api/schema/fastedge-config.test",
"appType": "proxy-wasm",
"request": {
"method": "GET",
"url": "https://example.com/",
"headers": {},
"body": ""
},
"properties": {}
},
"valid": true
}Error Responses
| Status | Condition |
|---|---|
404 |
fastedge-config.test.json does not exist |
Saves the provided configuration to .fastedge-debug/fastedge-config.test.json (relative to WORKSPACE_PATH if set, otherwise the current working directory). If the config includes a properties field, a WebSocket event is broadcast to connected clients.
Accepts an optional X-Source request header.
Request Body
{
config: TestConfig; // See GET /api/config for the TestConfig type
}The config object must match one of the two TestConfig variants. properties and appType are required in both variants; request is required and its shape depends on appType (path for "http-wasm", url for "proxy-wasm").
Response
{
ok: true;
}Example
curl -X POST http://localhost:5179/api/config \
-H "Content-Type: application/json" \
-H "X-Source: api" \
-d '{
"config": {
"$schema": "http://localhost:5179/api/schema/fastedge-config.test",
"appType": "proxy-wasm",
"request": {
"method": "GET",
"url": "https://example.com/",
"headers": { "accept": "text/html" },
"body": ""
},
"properties": {
"client.geo.country": "US"
}
}
}'{
"ok": true
}Error Responses
| Status | Condition |
|---|---|
400 |
Validation failed (missing config.appType, config.request, or config.properties) |
500 |
File write failed |
Saves the provided configuration to a file path previously vended by the (Electron-only) save dialog. The path must have been registered by /api/config/show-save-dialog first; unregistered paths are rejected. The path is single-use — it is removed from the pending set once consumed. Creates intermediate directories as needed.
Request Body
{
config: object; // The configuration object to serialize as JSON
filePath: string; // A path previously returned by the save dialog
}Response
{
ok: true;
savedPath: string; // The file path where the config was written
}Example
curl -X POST http://localhost:5179/api/config/save-as \
-H "Content-Type: application/json" \
-d '{
"config": {
"appType": "proxy-wasm",
"request": {
"method": "GET",
"url": "https://example.com/",
"headers": {},
"body": ""
},
"properties": {}
},
"filePath": "/home/user/project/configs/staging.test.json"
}'{
"ok": true,
"savedPath": "/home/user/project/configs/staging.test.json"
}Error Responses
| Status | Condition |
|---|---|
400 |
Missing config or filePath |
403 |
filePath was not vended by the save dialog (or was already used) |
500 |
File write or directory creation failed |
Serves a JSON Schema file by name. Use these schemas for request validation in test tooling or editor integrations.
The :name parameter is the schema name without the .schema.json suffix.
Response
Returns the JSON Schema document with Content-Type: application/json.
Available Schemas
| Name | Description |
|---|---|
api-load |
Request body schema for POST /api/load |
api-send |
Request body schema for POST /api/send |
api-call |
Request body schema for POST /api/call |
api-config |
Request body schema for POST /api/config |
| Name | Description |
|---|---|
fastedge-config.test |
Schema for fastedge-config.test.json config files |
hook-result |
Shape of a single HookResult object |
hook-call |
Shape of a HookCall input object |
full-flow-result |
Shape of the FullFlowResult returned by full-flow endpoints |
http-request |
Shape of an HttpRequest for HTTP-WASM execution |
http-response |
Shape of an HttpResponse returned by HTTP-WASM execution |
Example
curl http://localhost:5179/api/schema/api-sendcurl http://localhost:5179/api/schema/fastedge-config.testUsing the schema in a config file
{
"$schema": "http://localhost:5179/api/schema/fastedge-config.test",
"appType": "proxy-wasm",
"request": {
"method": "GET",
"url": "https://example.com/",
"headers": {},
"body": ""
},
"properties": {}
}Error Responses
| Status | Condition |
|---|---|
404 |
Schema name not found |
All error responses follow a consistent shape:
{
ok: false;
error: string | { formErrors: string[]; fieldErrors: Record<string, string[]> };
}When a request body fails schema validation (Zod), error is the flattened Zod error object with formErrors and fieldErrors. For runtime errors, error is a plain string.
Common status codes
| Status | Meaning |
|---|---|
400 |
Invalid request body, missing required fields, or precondition not met (e.g. no WASM loaded) |
403 |
Host/token check failed, or a save-as path was not vended by the save dialog |
404 |
Resource not found (config file, schema file) |
500 |
Internal server error during execution or I/O |
- WEBSOCKET.md — WebSocket protocol and event types
- DEBUGGER.md — Server startup, configuration, and environment variables
- TEST_FRAMEWORK.md — Programmatic test framework API
- RUNNER.md — Runner API and WASM type detection