Interception & Rewriting Capabilities

Each example below is a Rule JSON that relay-core rules validate accepts (and that PUT /api/v1/rules stores). The shape matches the Rule API: active, stage, termination, a tagged filter, and an actions array. Action fields live under config.

Mock Response

Replace the upstream response with a local one. Useful for frontend work, contract tests, and error simulation. Stopping at the request-header stage means the upstream is never contacted.

{
  "id": "mock-health",
  "name": "Mock /v1/health",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "Path", "config": { "mode": "Exact", "value": "/v1/health" } },
  "actions": [{
    "type": "MockResponse",
    "config": {
      "status": 200,
      "headers": { "content-type": "application/json" },
      "body": { "type": "Text", "value": "{\\"ok\\":true}" }
    }
  }]
}

Map Local

Serve the request from a local file. The path must stay inside the policy sandbox root.

{
  "id": "map-local-static",
  "name": "Serve /static from disk",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "Path", "config": { "mode": "Prefix", "value": "/static/" } },
  "actions": [{
    "type": "MapLocal",
    "config": { "path": "/Users/me/projects/app/static", "content_type": null }
  }]
}

Map Remote

Rewrite the request URL to another origin.

{
  "id": "map-remote-prod-to-staging",
  "name": "api.example.com -> staging",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "Host", "config": { "mode": "Exact", "value": "api.example.com" } },
  "actions": [{
    "type": "MapRemote",
    "config": { "url": "https://api-staging.example.com", "preserve_host": false }
  }]
}

Redirect

Return a 3xx redirect.

{
  "id": "redirect-legacy",
  "name": "/old -> /new",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "Path", "config": { "mode": "Exact", "value": "/old" } },
  "actions": [{
    "type": "Redirect",
    "config": { "location": "https://example.com/new", "status": 301 }
  }]
}

Header modification

Add, update, or delete request and response headers. UpdateRequestHeader / UpdateResponseHeader accept {{previous}} in value.

{
  "id": "strip-cookie",
  "name": "Strip Set-Cookie on /api/*",
  "active": true,
  "stage": "ResponseHeaders",
  "priority": 100,
  "termination": "Continue",
  "filter": { "type": "Path", "config": { "mode": "Prefix", "value": "/api/" } },
  "actions": [{ "type": "DeleteResponseHeader", "config": { "name": "set-cookie" } }]
}
{
  "id": "add-debug",
  "name": "Add X-Debug header",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Continue",
  "filter": { "type": "Method", "config": { "mode": "Exact", "value": "POST" } },
  "actions": [{
    "type": "AddRequestHeader",
    "config": { "name": "x-debug", "value": "relay-core" }
  }]
}

Body modification

Replace a body, run a regex, or set a JSONPath. Body-dependent rules belong on RequestBody / ResponseBody. A body over the inspect budget is skipped with a recorded reason; it is not matched against the truncated prefix.

{
  "id": "jsonpath-set",
  "name": "Force user.role = admin",
  "active": true,
  "stage": "RequestBody",
  "priority": 100,
  "termination": "Continue",
  "filter": { "type": "Host", "config": { "mode": "Exact", "value": "api.example.com" } },
  "actions": [{
    "type": "TransformRequestBody",
    "config": {
      "transform": {
        "type": "JsonPathSet",
        "config": { "path": "$.user.role", "value": "\\"admin\\"" }
      }
    }
  }]
}
{
  "id": "regex-replace",
  "name": "Replace token in body",
  "active": true,
  "stage": "ResponseBody",
  "priority": 100,
  "termination": "Continue",
  "filter": { "type": "Path", "config": { "mode": "Exact", "value": "/login" } },
  "actions": [{
    "type": "TransformResponseBody",
    "config": {
      "transform": {
        "type": "RegexReplace",
        "config": { "pattern": "secret=([^&]+)", "replacement": "secret=REDACTED" }
      }
    }
  }]
}

Intercept breakpoints

Pause live traffic for a human decision. Create a breakpoint with POST /api/v1/intercepts or MCP set_intercept; release or drop it with resume / resume_flow. There is no relay-core intercept subcommand.

curl -X POST http://127.0.0.1:8082/api/v1/intercepts \
  -H 'Content-Type: application/json' \
  -d '{
    "url_pattern": "api.example.com",
    "phase": "request"
  }'

Hits show up in the TUI Intercepts panel and via GET /api/v1/intercepts. request_header_upserts / response_header_upserts add or overwrite by name and keep the rest. request_headers / response_headers replace the whole header map.

curl -X POST http://127.0.0.1:8082/api/v1/intercepts/<key>/resume \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "continue",
    "request_header_upserts": { "x-test": "value" }
  }'

Modifiable fields: method, url, request_headers, request_header_upserts, request_body, status_code, response_headers, response_header_upserts, response_body, message_content (WebSocket).

Flow replay

Re-send a captured HTTP request as it was stored. This call cannot edit headers or body. The replay enters through the running proxy, so it is captured as a new flow and the current rules and scripts apply. The call fails when the proxy is not running. HTTP only; WebSocket flows are rejected.

curl -X POST http://127.0.0.1:8082/api/v1/flows/<id>/replay

# Skip TLS verification (debug only)
curl -X POST 'http://127.0.0.1:8082/api/v1/flows/<id>/replay?accept_invalid_certs=true'

WebSocket message rewriting

Two actions apply only at stage WebSocketMessage:

{
  "id": "ws-mock",
  "name": "Mock chat messages",
  "active": true,
  "stage": "WebSocketMessage",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "Host", "config": { "mode": "Exact", "value": "chat.example.com" } },
  "actions": [{
    "type": "MockWebSocketMessage",
    "config": {
      "direction": "Incoming",
      "message": "{\\"type\\":\\"welcome\\",\\"user\\":\\"bot\\"}"
    }
  }]
}

Or drop matching messages:

{
  "id": "ws-drop-pings",
  "name": "Drop ping frames",
  "active": true,
  "stage": "WebSocketMessage",
  "priority": 100,
  "termination": "Stop",
  "filter": { "type": "All" },
  "actions": [{ "type": "DropWebSocketMessage" }]
}

L3/L4 actions

These three actions only apply at the Connect stage:

  • RedirectIp — dial a different IP
  • ForwardPort — forward to another host:port (hostnames are resolved)
  • SetTtl — accepted, then logged; the TTL on the wire does not change