Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

Welcome to Roxy — a programmable MITM proxy for HTTP(S), HTTP/2, HTTP/3, WebSockets, and more.
Roxy makes it easy to inspect, rewrite, and automate traffic using Rust, Lua, JavaScript, or Python scripting engines.


Prerequisites

  • Rust 1.80 or newer
  • Cargo
  • (optional) Node.js if you want to test JavaScript interceptors
  • (optional) Python 3 if you want Python scripting
  • (optional) Lua 5.4 for Lua scripting

Installation

Clone the repository and build from source:

git clone https://github.com/fergdev/roxy.git
cd roxy
cargo build --release

Scripting Reference

This section is the user-facing API: how to manipulate flows, requests, responses, headers, and queries from Lua/JS/Python.

All code snippets have language tabs. Pick your language once; it’ll stick across pages.

Flows

A Flow is the object your scripts receive for each HTTP exchange. It exposes:

  • flow.request: the incoming request (method, URL, headers, body, trailers, version)
  • flow.response: the outgoing response (status, headers, body, trailers, version)

You can read/modify either side during interception.


Interception lifecycle

  • Request phase: runs before the upstream request is sent.
    You can rewrite method, URL, headers, or body — or even synthesize a response and short-circuit the request.

  • Response phase: runs after a response is available.
    You can edit status, headers, body, or trailers before the client sees it.


Examples

{{#tabs global="language"}} {{#tab name="Lua"}}

-- Request interception
function request(flow)
  local h = flow.request.headers
  print(h["host"])
  h["X-Trace"] = "abc123"

  -- Rewrite body
  flow.request.body.text = flow.request.body.text .. " appended from Lua"
end

-- Response interception
function response(flow)
  local h = flow.response.headers
  h["Server"] = "LuaProxy"
  flow.response.body.text = "overwritten response"
end

{{#endtab}} {{#tab name=“JS”}}// Request interception

function request(flow) {
  const h = flow.request.headers;
  console.log(h.get("host"));
  h.set("X-Trace", "abc123");

  flow.request.body.text = flow.request.body.text + " appended from JS";
}

function response(flow) {
  const h = flow.response.headers;
  h.set("Server", "JsProxy");
  flow.response.body.text = "overwritten response";
}

{{#endtab}} {{#tab name=“Python”}}

def request(flow):
    h = flow.request.headers
    print(h["host"])
    h["X-Trace"] = "abc123"

    # Rewrite body
    flow.request.body = flow.request.body + " appended from Python"

# Response interception
def response(flow):
    h = flow.response.headers
    h["Server"] = "PyProxy"
    flow.response.body = "overwritten response"

{{#endtab}} {{#endtabs}}

Request

Response

Headers

HTTP headers are case-insensitive, order-preserving, and allow multiple fields with the same name.

Quick API

  • h[name] / h.get(name) → folded string (values joined with ", ").
  • h[name] = value / h.set(name, value) → replace all fields for that name.
  • del h[name] / h.delete(name) → remove all fields for that name.
  • h.get_all(name) → list of values in order.
  • h.set_all(name, values) → explicit multi-field set.
  • h.insert(index, name, value) → insert raw field at index.
  • h.items(multi=false) → iterate (raw if multi=true).

Common tasks

Read/Write a header

{{#tabs global="language"}} {{#tab name="Lua"}}

local h = flow.request.headers
print(h["host"])
h["X-Trace"] = "abc123"

{{#endtab}} {{#tab name=“JS”}}

const h = flow.request.headers;
console.log(h.get("host"));
h.set("X-Trace", "abc123");

{{#endtab}}

{{#tab name=“Python”}}

h = flow.request.headers
print(h["host"])
h["X-Trace"] = "abc123"

{{#endtab}} {{#endtabs}}

Query

Notify

Specs

Language-agnostic contracts for all public runtime APIs. Each spec includes:

  • Normative requirements
  • API surface
  • Language contracts (Lua/JS/Python)
  • Numbered conformance tests to implement per language

Specs

Headers (Spec)

A case-insensitive, order-preserving header collection that permits multiple fields of the same name and renders to an HTTP/1 header block.

1) Requirements (MUST)

  • Keys are case-insensitive for lookup/mutation (ASCII lowercase comparison).
  • Raw order preserved (insertion order of fields).
  • Multiple fields with the same name are allowed.
  • Byte rendering uses HTTP/1 header line format: Name: value\r\n per field, no trailing blank line.

2) API (language-agnostic)

  • h[name] -> str — folded value (join with ", ").
  • h[name] = str|bytes — replace all fields for name with a single field appended at end.
  • del h[name] — remove all fields for name.
  • get_all(name) -> list[str] — raw values in order (no folding).
  • set_all(name, values: Iterable[str|bytes]) — explicit multi-fields (append in order).
  • insert(index, name, value) — insert raw field at index (0-based).
  • items(multi=false) — iterator:
    • false: logical (folded) items in order of first appearance by lowercase name.
    • true: raw (name, value) pairs in stored order.
  • bytes(h) / toBytes(h) — CRLF per line, no final blank line.

3) Language contracts

{{#tabs global="language"}}

{{#tab name="Lua"}}

TopicAPI
TypeHeaders userdata
ConstructHeaders.new({{"Name","value"}, ...})
Accessh["Name"], h["Name"] = v, h["Name"] = nil
Methodsh:get_all(name), h:set_all(name, vals), h:insert(i,n,v), h:items(multi), h:to_bytes()
{{#endtab}}

{{#tab name="JS"}}

TopicAPI
Typeclass Headers
Constructnew Headers([["Name","value"], ...])
Accessh.get(name), h.set(name, value), h.delete(name)
MethodsgetAll, setAll, insert, items(multi=false), toBytes
{{#endtab}}

{{#tab name="Python"}}

TopicAPI
Typeclass Headers
ConstructHeaders([(b"Name", b"value"), ...])
Accessh["Name"], h["Name"] = v, del h["Name"]
Methodsget_all, set_all, insert, items(multi=False), bytes(h)
{{#endtab}}

{{#endtabs}}

4) Conformance tests (H-01…H-08)

Implement these black-box tests in each language. When all pass, tick the checklist.

H-01 Case-insensitive lookup & fold
Given: [("Host","example.com"),("accept","text/html"),("ACCEPT","application/xml")]
Expect:

  • get("host") == "example.com"
  • get("Accept") == "text/html, application/xml"
  • get_all("accept") == ["text/html","application/xml"]

H-02 Replace via assignment / set
From H-01, set "Accept" = "application/json"get_all("accept") == ["application/json"]; raw ends with one Accept.

H-03 set_all order
set_all("Set-Cookie", ["a=1","b=2"])get_all("set-cookie") == ["a=1","b=2"]; folded "a=1, b=2".

H-04 delete
Delete "Accept"get_all("accept") == [], others intact.

H-05 insert at index
insert(1, "X-Debug", "on") → raw index 1 equals ("X-Debug","on").

H-06 bytes rendering
Rendered bytes are joined Name: value\r\n per raw field, no extra blank line.

H-07 items(multi=false)
Yields folded logical items in first-appearance order by lowercase name.

H-08 mixed str/bytes
Accept both str/bytes for input; read returns string (Lua/JS) or str (Py).

5) Checklist

  • Lua
  • JS
  • Python

Request

Response

Testing