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

Roxy — Certificate Authority (CA) and installing the CA certificate

Roxy creates a local Certificate Authority (CA) the first time it runs. That CA is used to generate short-lived leaf certificates on the fly so Roxy can intercept and inspect HTTPS traffic. Browsers and OSes do not trust this CA by default, so to avoid TLS warnings you’ll want to install the Roxy CA cert into the appropriate trust store on the machine or device you’re testing.

Below you’ll find:

  • What files Roxy writes
  • Short descriptions of each file and recommended permissions
  • How to use the certs from command line tools (curl/wget) while developing
  • Platform-specific installation instructions (macOS, Linux, Windows, iOS, Android, Java, browsers)
  • Security notes and quick troubleshooting tips

Files created by Roxy

When Roxy generates the CA it stores a small set of files in the config directory (e.g. ~/.roxy by default). Example listing:

.rw-r--r--  1.2k  20 Sep 07:23  roxy-ca-cert.cer
.rw-r--r--  1.2k  20 Sep 07:24  roxy-ca-cert.p12
.rw-r--r--  1.2k  20 Sep 07:23  roxy-ca-cert.pem
.rw-r--r--  2.9k  20 Sep 07:23  roxy-ca.cer
.rw-r--r--  2.7k  20 Sep 07:24  roxy-ca.p12
.rw-r--r--  2.9k  20 Sep 07:23  roxy-ca.pem

What each file is for

FilenameUse
roxy-ca.pemThe CA certificate and private key in PEM format (combined). Keep this private — it contains the private key.
roxy-ca-cert.pemThe CA certificate only (PEM). Use this to import into most OS and app trust stores.
roxy-ca-cert.p12The CA certificate in PKCS#12 format (contains cert and private key). Useful for systems that expect a .p12 bundle. Protect this file.
roxy-ca-cert.cerA certificate file with a .cer extension (PEM-encoded). Some devices expect .cer when installing.
roxy-ca.cer
roxy-ca.p12
roxy-ca.pem
Alternate names / copies that some tooling expects; the .cer is functionally the cert, .p12 is PKCS#12 bundle, .pem is PEM-encoded. Roxy writes multiple file extensions for maximum compatibility.

Permissions recommendation: make the private-key-containing files readable only by you:

chmod 600 ~/.roxy/roxy-ca.pem
chmod 600 ~/.roxy/roxy-ca-cert.p12

Why Roxy generates a CA and why it’s local-only

Roxy generates a unique CA on first run so that your intercepted traffic stays private to your machine. The CA private key is never shared between installations — this prevents another machine’s Roxy instance from being able to decrypt your traffic. If the CA private key is ever compromised, you should remove the CA from trust stores and regenerate a new CA.

Quick CLI examples (using Roxy as proxy)

If you just want to test a single HTTPS request through Roxy without installing the CA globally, pipe the roxy-ca-cert.pem to your tools:

curl

curl --proxy 127.0.0.1:8080 --cacert ~/.roxy/roxy-ca-cert.pem <https://example.com/>

wget

wget -e https_proxy=127.0.0.1:8080 --ca-certificate ~/.roxy/roxy-ca-cert.pem <https://example.com/>

Replace 127.0.0.1:8080 with the host:port your Roxy instance listens on.

Platform installation guide

Note: exact UI steps vary by OS version. When possible prefer importing the PEM (roxy-ca-cert.pem) into the system trust store rather than a per-user store, especially for browsers and system services.

macOS (System-wide)

Install and trust the certificate in the macOS system keychain:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.roxy/roxy-ca-cert.pem

You can also double-click roxy-ca-cert.pem to open Keychain Access, add it to the System keychain, then open the certificate and set Trust → When using this certificate → Always Trust.

Ubuntu / Debian

For command-line tools and most system services:

sudo cp ~/.roxy/roxy-ca-cert.pem /usr/local/share/ca-certificates/roxy-ca.crt
sudo update-ca-certificates

Fedora / RHEL / CentOS

sudo cp ~/.roxy/roxy-ca-cert.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

Arch Linux

sudo trust anchor --store ~/.roxy/roxy-ca-cert.pem

(or use ca-certificates package instructions for your distro variant).

Mozilla Firefox (Linux / macOS / Windows)

Firefox maintains its own certificate store. Import the roxy-ca-cert.pem via: Preferences → Privacy & Security → View Certificates → Authorities → Import… and enable trusting for websites.

Alternatively, use certutil (from nss-tools / libnss3-tools) to import into a profile programmatically.

Google Chrome on Linux

If Chrome is built to use the system store, installing into the system CA (see Debian/Ubuntu steps) is enough. If not, you may need to import into the NSS DB used by Chrome (see certutil usage for your distro).

Windows

Import the .cer into the Windows Trusted Root Certification Authorities store: • Double-click roxy-ca-cert.cer → Install Certificate → Local Machine → Place in Trusted Root Certification Authorities. • Or use certutil (run as admin):

certutil -addstore -f "ROOT" C:\path\to\roxy-ca-cert.cer

If you need a PKCS#12 bundle for some Windows tools or browsers, use roxy-ca-cert.p12.

iOS (real devices)

On recent iOS versions you must both install and enable full trust:

  1. Copy roxy-ca-cert.cer to the device (e.g., send via email or host on a local webserver).
  2. Open the file on the device; iOS will add the profile in Settings → General → VPN & Device Management (or Profiles).
  3. After installing, go to Settings → General → About → Certificate Trust Settings and enable full trust for the installed Roxy certificate.

iOS Simulator

  1. Ensure macOS is configured to proxy the simulator network through Roxy.
  2. In the simulator, open Safari and visit a URL that serves roxy-ca-cert.cer (you can host on localhost and use macOS forwarding).
  3. Install the cert in the simulator settings and enable full trust (see real device steps).

Android (device)

Android behaviour differs between versions: • User-installed CA certs are not trusted by all apps on Android 7+ by default (apps can opt out of user CAs). For system-wide trust you must install the cert to the system store — this requires root or building the cert into the system image. • For development and testing on devices: • Convert the PEM to DER if needed:

openssl x509 -in ~/.roxy/roxy-ca-cert.pem -outform DER -out roxy-ca-cert.der
  • Copy roxy-ca-cert.cer / .der to the device and install via Settings → Security → Install from storage (UI varies).
  • For emulators you can push the cert into the emulator system store or use the simulator-device instructions.

Java (JVM)

To add the Roxy CA to the JVM cacerts store (system-wide JDK):

sudo keytool -importcert -trustcacerts -alias roxy -file ~/.roxy/roxy-ca-cert.pem \
  -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit

(Replace $JAVA_HOME with your JDK path and adjust -storepass if your cacerts password differs.)

Creating a .p12 or .cer from PEM (if needed)

If you only have a PEM and need a PKCS#12 bundle (for Windows/macOS imports):

openssl pkcs12 -export -out roxy-ca-cert.p12 -inkey roxy-ca.pem -in roxy-ca-cert.pem -passout pass:changeit

To convert PEM to DER (.der/.cer) for Android:

openssl x509 -in roxy-ca-cert.pem -outform DER -out roxy-ca-cert.der

Verifying the certificate fingerprint

Always verify the certificate fingerprint before trusting/distributing it:

openssl x509 -in ~/.roxy/roxy-ca-cert.pem -noout -fingerprint -sha256

Example output: SHA256 Fingerprint=AA:BB:CC:...:ZZ

Publish the fingerprint (or display it in your UI) so users can confirm they installed the correct CA.

Security recommendations

  • Never distribute roxy-ca.pem or roxy-ca-cert.p12 (both contain the private key) unless absolutely necessary and only over a secure channel.
  • Keep private-key files (roxy-ca.pem, *.p12) permissioned to owner-only: chmod 600.
  • If the machine is shared or exposed, revoke and regenerate the CA and remove it from any systems it was installed into.
  • Log and audit where you installed the CA so you can revoke/trust changes consistently.
  • Prefer per-development-machine CA rather than a shared global CA for testing.

Troubleshooting

  • Still seeing TLS warnings: confirm the cert was installed in the system trust store (not just the browser’s temporary profile). Check the cert fingerprint.
  • Browser still rejects after installing system CA: some browsers (Firefox on many platforms) use their own certificate store — import into Firefox separately.
  • Android app refuses: modern Android apps may opt out of user CAs — either install CA into system store (requires root) or configure the app/network stack to trust the certificate for development.
  • Java apps fail: ensure the CA is in the JVM cacerts used by that runtime. Different JDKs/containers may have separate cacerts files.
  • Proxy not being used: ensure your client is sending traffic via Roxy (check host/port, and that the proxy accepts TLS CONNECT or is configured as a transparent proxy).

Example checklist for onboarding a new dev machine

  1. Start Roxy once to allow it to generate ~/.roxy/* files.
  2. Verify fingerprint:
openssl x509 -in ~/.roxy/roxy-ca-cert.pem -noout -fingerprint -sha256
  1. Install roxy-ca-cert.pem into your OS/browser as required (use steps above).
  2. Confirm with curl:
curl --proxy 127.0.0.1:8080 --cacert ~/.roxy/roxy-ca-cert.pem <https://example.com/> -v

If TLS succeeds without cert warnings, you’re good.

Scripting Reference

Roxy includes a composable scripting system so you can extend, automate and prototype protocol logic without rebuilding the proxy. Roxy exposes three script engines that cover a wide range of use cases:

  • JavaScript — a familiar, full-featured runtime for ecosystem libraries and concise logic.
  • Lua — lightweight and fast for tiny hooks and quick iteration.
  • Python — expressive and powerful for complex processing and integrations.

This section explains the extensions model, shows concise examples in each language, and gives practical tips so you can copy/paste and get started quickly.

Core concepts

  • Extensions / scripts register callbacks for events (for example request, response, connect, tls_handshake) and can inspect, mutate, or replace flows.
  • Options are configuration knobs a script can expose; Roxy surfaces those in the config file, CLI, and UI.
  • Commands are functions a script exposes that users can invoke interactively (or bind to keys).
  • Scripts can be loaded at startup or attached dynamically depending on your run mode.
  • Scripts run in their engine sandbox and are invoked for each matching event.

Security note: scripts can access request/response data and, depending on host policy, filesystem or network APIs. Treat third-party scripts like local code — review them before enabling in shared environments.

Running scripts

Roxy accepts scripts on the CLI or via roxy.toml:

roxy --script ./examples/extensions/counter.py

Anatomy of an extension.

A Roxy extension is just a script implementing one or more event handlers. Handlers are ordinary functions (or methods on an exported object) named for the event they handle.

Common events:

  • start() / stop() — lifecycle hooks
  • request(flow) — before a request is sent upstream
  • response(flow) — after a response is received (before returning to client)
  • connect(flow) / tls_handshake(flow) — low-level transport events
  • error(ctx) — runtime errors or engine-level notifications

Roxy converts types to idiomatic host-language objects (tables in Lua, dict-like objects in Python, plain objects in JS). The API surface aims to be consistent across engines.

Counter example

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

globalThis.extensions = [{
  start() {
    this.count = 0;
    console.log("counter started");
  },

  request(flow) {
    this.count += 1;
    console.log(`seen ${this.count} requests`);
  }
}];

{{#endtab}} {{#tab name=Lua}}

local count = 0
Extensions = {
 {
  start = function(self)
    count = 0
    print("counter started")
  end,
  request = function request(flow)
    count = count + 1
    print("seen %d requests", self.count)
  end
 },
}

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

class Counter:
    def __init__(self):
        self.count = 0

    def start(self):
        print("counter started")
        self.count = 0

    def request(self, flow):
        self.count += 1
        print("seen %d requests", self.count)

Extensions = [Counter()]

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

Flow object basics

All engines receive a flow object representing a transaction. Typical fields and helpers:

  • flow.request — request (method, url, headers, body, scheme, host, port)
  • flow.response — response (status_code, headers, body)
  • flow.client_addr, flow.server_conn — transport metadata
  • Helpers: flow.reply(), flow.kill(), flow.replace(), flow.resume() — control flow lifecycle

Bindings convert types to idiomatic objects:

  • Python: mapping-like headers, bytes-like bodies
  • JS: plain objects and strings/ArrayBuffers
  • Lua: table-like headers and strings

Identical behavior in 3 languages

Add header x-roxy-example: true to every request.

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

function request(flow) {
  flow.request.headers["x-roxy-example"] = "true";
}
module.exports = { request };

{{#endtab}} {{#tab name=Lua}}

function request(flow)
  flow.request.headers["x-roxy-example"] = "true"
end

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

def request(flow):
    flow.request.headers["x-roxy-example"] = "true"

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

Debugging and tips

  • Use the engine’s logging APIs (console.log, print, Python logging) to surface runtime info.
  • Start with module-level handlers for quick iteration; move to object/class form for stateful addons.
  • Keep scripts focused — split complex logic across multiple scripts.
  • For high-performance paths, prefer Lua or precompiled JS; heavy CPU work should run in native code or an external service.
  • Be mindful of concurrency: handlers may be invoked from multiple workers; rely on documented concurrency rules or use engine-provided sync primitives.

Packaging & sharing

  • Store small scripts in examples/addons/ inside the repo for easy teammate access.
  • Version scripts and document their options/commands so upgrades are predictable.
  • Consider providing a roxy-scripts collection with utilities and standard helpers for manipulating flows.

Quick checklist for adding a script to the repo

  1. Add the script under examples/addons/ (e.g., examples/addons/add_header.py).
  2. Test locally:
roxy --script ./examples/addons/add_header.py
  1. Verify behavior with a curl request through Roxy:
curl --proxy 127.0.0.1:8080 -v <https://example.com/>
  1. Commit and add a short README describing the script, its options, and any commands.

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=JS}}

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=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=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.

API

Set header

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

flow.request.headers.set("X-Header1", "request");

{{#endtab}} {{#tab name=Lua}}

flow.request.headers:set("X-Header1", "request")

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

flow.request.headers.set("X-Header1", "request")

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

Append Header

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

flow.request.headers.append("X-Header1", "request");

{{#endtab}} {{#tab name=Lua}}

flow.request.headers:append("X-Header1", "request")

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

flow.request.headers.append("X-Header1", "request")

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

Remove a header

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

flow.request.headers.delete("X-Header1");
flow.request.headers.set("X-header2", undefined);
flow.request.headers.set("X-header3", null);

{{#endtab}} {{#tab name=Lua}}

flow.request.headers:delete("X-Header");
flow.request.headers["X-Header2"] = nil

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

flow.request.headers.delete("X-Header1")
flow.request.headers["X-Header2"] = None
del flow.request.headers["X-header3"]

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

Test if value is present

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

if (flow.request.headers.has("X-Header")) {
  console.log("header is present");
}

{{#endtab}} {{#tab name=Lua}}

if (flow.request.headers:has("X-Header")) then
  print("header is present")
end

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

if flow.request.headers.has("X-Header"):
  print("header is present")

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

Body

HTTP bodies can be binary or text, may be empty, and can be read or replaced by scripts.

API

Set text

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

flow.request.body.text = "new request body";
flow.response.body.text = "new response body";

{{#endtab}} {{#tab name=Lua}}

flow.request.body.text = "new request body"
flow.response.body.text = "new response body"

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

flow.request.body.text = "new request body"
flow.response.body.text = "new response body"

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

Get text

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

let req_text = flow.request.body.text;
let res_body = flow.response.body.text;

{{#endtab}} {{#tab name=Lua}}

local req_text = flow.request.body.text;
local res_body = flow.response.body.text;

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

req_text = flow.request.body.text;
res_body = flow.response.body.text;

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

Set bytes

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

flow.request.body.bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);

{{#endtab}} {{#tab name=Lua}}

flow.request.body.bytes = string.char(0xde, 0xad, 0xbe, 0xef)

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

flow.request.body.bytes = b"\xde\xad\xbe\xef"

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

Get bytes

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

const bytes = flow.request.body.bytes

{{#endtab}} {{#tab name=Lua}}

local bytes = flow.request.body.bytes

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

bytes = flow.request.body.bytes

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

Clear body

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

flow.request.body.bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);

{{#endtab}} {{#tab name=Lua}}

flow.request.body.bytes = string.char(0xde, 0xad, 0xbe, 0xef)

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

flow.request.body.bytes = b"\xde\xad\xbe\xef"

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

Length

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

const len = flow.request.body.len

{{#endtab}} {{#tab name=Lua}}

local len = flow.request.body.len

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

len = flow.request.body.len

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

Is empty

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

const isEmpty = flow.request.body.isEmpty

{{#endtab}} {{#tab name=Lua}}

local isEmpty = flow.request.body.isEmpty

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

isEmpty = flow.request.body.isEmpty

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

URL

URL is a parameter on thew Flow.response object that allows you to get or set the URL of the request.

API

Host

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

if (flow.request.url.host == "localhost:1234") {
  flow.request.url.host = "example.com:4321"
}

{{#endtab}} {{#tab name=Lua}}

if (flow.request.url.host == "localhost:1234") then
  flow.request.url.host = "example.com:4321"
end

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

if flow.request.url.host == "localhost:1234":
    flow.request.url.host = "example.com:4321"

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

Hostname

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

if (flow.request.url.host == "localhost") {
  flow.request.url.host = "example.com"
}

{{#endtab}} {{#tab name=Lua}}

if (flow.request.url.host == "localhost") then
  flow.request.url.host = "example.com"
end

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

if flow.request.url.host == "localhost":
    flow.request.url.host = "example.com"

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

Port

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

if (flow.request.url.port == 80) {
  flow.request.url.port = 8080
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.port == 80 then
  flow.request.url.port = 8080
end

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

if flow.request.url.port == 80:
    flow.request.url.port = 8080

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

Username

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

if (flow.request.url.username == "dave") {
  flow.request.url.username = "damo";
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.username == "dave" then
  flow.request.url.username = "damo"
end

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

if flow.request.url.username == "dave":
    flow.request.url.username = "damo"

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

Password

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

if (flow.request.url.password == "1234") {
  flow.request.url.password = "abcd";
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.password == "1234" then
  flow.request.url.password = "abcd"
end

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

if flow.request.url.password == "1234":
    flow.request.url.password = "abcd"

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

Authority

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

if (flow.request.url.authority == "dave:1234@localhost:1234") {
  flow.request.url.authority = "damo:abcd@localhost:4321";
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.authority == "dave:1234@localhost:1234" then
  flow.request.url.authority = "damo:abcd@localhost:4321"
end

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

if flow.request.url.authority == "dave:1234@localhost:1234":
    flow.request.url.authority = "damo:abcd@localhost:4321"

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

Path

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

if (flow.request.url.path == "/some/path") {
  flow.request.url.path = "/another/path";
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.path == "/some/path" then
  flow.request.url.path = "/another/path"
end

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

if flow.request.url.path == "/some/path":
    flow.request.url.path = "/another/path"

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

Scheme

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

if (flow.request.url.protocol == "http") {
  flow.request.url.protocol = "https";
}

{{#endtab}} {{#tab name=Lua}}

if flow.request.url.scheme == "http" then
  flow.request.url.scheme = "https"
end

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

if flow.request.url.scheme == "http":
    flow.request.url.scheme = "https"

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

Query

Constants

Roxy provides a number of predefined constants to make scripting easier.
These constants are available in Lua, Python, and JavaScript (Boa) engines.


Method

HTTP request methods:

Method = {
  CONNECT = "CONNECT",
  DELETE  = "DELETE",
  GET     = "GET",
  HEAD    = "HEAD",
  OPTIONS = "OPTIONS",
  PATCH   = "PATCH",
  POST    = "POST",
  PUT     = "PUT",
  TRACE   = "TRACE",
}
if flow.request.method == Method.GET then
  flow.request.method = Method.POST
end

Protocol

Protocol = {
  HTTP  = "http",
  HTTPS = "https",
}
if flow.request.url.protocol == Protocol.HTTP then
  flow.request.url.protocol = Protocol.HTTPS
end

Version

Version = {
  HTTP09 = "HTTP/0.9",
  HTTP10 = "HTTP/1.0",
  HTTP11 = "HTTP/1.1",
  HTTP2  = "HTTP/2",
  HTTP3  = "HTTP/3",
}
if flow.request.version == Version.HTTP11 then
  flow.request.version == Version.HTTP10
end

Constants

Status = {
  CONTINUE                      = 100,
  SWITCHING_PROTOCOLS           = 101,
  PROCESSING                    = 102,
  OK                            = 200,
  CREATED                       = 201,
  ACCEPTED                      = 202,
  NON_AUTHORITATIVE_INFORMATION = 203,
  NO_CONTENT                    = 204,
  RESET_CONTENT                 = 205,
  PARTIAL_CONTENT               = 206,
  MULTI_STATUS                  = 207,
  ALREADY_REPORTED              = 208,
  IM_USED                       = 226,
  MULTIPLE_CHOICES              = 300,
  MOVED_PERMANENTLY             = 301,
  FOUND                         = 302,
  SEE_OTHER                     = 303,
  NOT_MODIFIED                  = 304,
  USE_PROXY                     = 305,
  TEMPORARY_REDIRECT            = 307,
  PERMANENT_REDIRECT            = 308,
  BAD_REQUEST                   = 400,
  UNAUTHORIZED                  = 401,
  PAYMENT_REQUIRED              = 402,
  FORBIDDEN                     = 403,
  NOT_FOUND                     = 404,
  METHOD_NOT_ALLOWED            = 405,
  NOT_ACCEPTABLE                = 406,
  PROXY_AUTHENTICATION_REQUIRED = 407,
  REQUEST_TIMEOUT               = 408,
  CONFLICT                      = 409,
  GONE                          = 410,
  LENGTH_REQUIRED               = 411,
  PRECONDITION_FAILED           = 412,
  PAYLOAD_TOO_LARGE             = 413,
  URI_TOO_LONG                  = 414,
  UNSUPPORTED_MEDIA_TYPE        = 415,
  RANGE_NOT_SATISFIABLE         = 416,
  EXPECTATION_FAILED            = 417,
  IM_A_TEAPOT                   = 418,
  MISDIRECTED_REQUEST           = 421,
  UNPROCESSABLE_ENTITY          = 422,
  LOCKED                        = 423,
  FAILED_DEPENDENCY             = 424,
  TOO_EARLY                     = 425,
  UPGRADE_REQUIRED              = 426,
  PRECONDITION_REQUIRED         = 428,
  TOO_MANY_REQUESTS             = 429,
  REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
  UNAVAILABLE_FOR_LEGAL_REASONS = 451,
  INTERNAL_SERVER_ERROR         = 500,
  NOT_IMPLEMENTED               = 501,
  BAD_GATEWAY                   = 502,
  SERVICE_UNAVAILABLE           = 503,
  GATEWAY_TIMEOUT               = 504,
  HTTP_VERSION_NOT_SUPPORTED    = 505,
  VARIANT_ALSO_NEGOTIATES       = 506,
  INSUFFICIENT_STORAGE          = 507,
  LOOP_DETECTED                 = 508,
  NOT_EXTENDED                  = 510,
  NETWORK_AUTHENTICATION_REQUIRED = 511,
}
if response.status == Status.NOT_FOUND then
  notify(2, "Got a 404!")
end

Notify

Notifications can be sent to the UI through the notify API.

Notifications have 5 levels: debug, info, warning, error and trace. Filters can be set in the settings for filtering notifications.

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

flow.request.headers.set("X-Header1", "request");

{{#endtab}} {{#tab name=Lua}}

flow.request.headers:set("X-Header1", "request")

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

flow.request.headers.set("X-Header1", "request")

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

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