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 ifmulti=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 forname
with a single field appended at end.del h[name]
— remove all fields forname
.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 atindex
(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"}}
Topic | API |
---|---|
Type | Headers userdata |
Construct | Headers.new({{"Name","value"}, ...}) |
Access | h["Name"] , h["Name"] = v , h["Name"] = nil |
Methods | h:get_all(name) , h:set_all(name, vals) , h:insert(i,n,v) , h:items(multi) , h:to_bytes() |
{{#endtab}} |
{{#tab name="JS"}}
Topic | API |
---|---|
Type | class Headers |
Construct | new Headers([["Name","value"], ...]) |
Access | h.get(name) , h.set(name, value) , h.delete(name) |
Methods | getAll , setAll , insert , items(multi=false) , toBytes |
{{#endtab}} |
{{#tab name="Python"}}
Topic | API |
---|---|
Type | class Headers |
Construct | Headers([(b"Name", b"value"), ...]) |
Access | h["Name"] , h["Name"] = v , del h["Name"] |
Methods | get_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