A programmable HTTP/HTTPS proxy library for integration testing. Record real upstream traffic, replay it deterministically, inject stubbed responses, intercept and modify requests/responses via reqwest-style middleware, simulate failure conditions, and orchestrate multiple upstreams under shared control — both in-process (typed Rust API) and out-of-process (JSON-Lines over TCP).
The full design is in SPECIFICATION.md.
api-proxy/
├── crates/
│ ├── partly-proxy-lib/ # the library (crates.io target)
│ │ └── examples/host.rs # minimal env-var-driven hosting binary
│ └── partly-proxy-echo/ # deterministic test upstream (used by lib tests)
├── ts-client/ # TypeScript client (@partly/proxy-client, npm target)
├── scripts/
│ ├── test-unit.sh # fmt + clippy + cargo test
│ └── test-ts.sh # tsc + vitest
├── .github/workflows/ci.yml # CI delegates to scripts/*.sh
└── SPECIFICATION.md
use partly_proxy_lib::{Command,ProxyClusterBuilder,ProxyConfig,RecordingConfig,RequestMatcher,StubbedResponse,UpstreamTarget,};use bytes::Bytes;use http::{Method,StatusCode};use std::time::Duration;#[tokio::main]asyncfnmain() -> partly_proxy_lib::Result<()>{let cluster = ProxyClusterBuilder::new().recording(RecordingConfig::in_memory(10_000)).add_upstream("api",ProxyConfig::http("127.0.0.1:8080".parse().unwrap(),UpstreamTarget::new("https://api.upstream.example"),),).tcp_control_plane("127.0.0.1:4500".parse().unwrap()).run().await?;// Register a stub at runtime.
cluster
.command_sender().send(Command::Stub{upstream:Some("api".into()),matcher:RequestMatcher::new().method(Method::POST).path(r"^/orders/\d+/refund$"),response:StubbedResponse::new(StatusCode::CREATED).header("content-type","application/json").body(Bytes::from_static(b"{\"ok\":true}")),times:Some(3),}).await?;// ... drive your system under test against http://127.0.0.1:8080 ...
cluster.shutdown().await}import{ProxyClient}from"@partly/proxy-client";constclient=awaitProxyClient.connect({host: "127.0.0.1",port: 4500});awaitclient.stub({upstream: "api",method: "POST",path_pattern: "^/orders/\\d+/refund$",status: 201,body: '{"ok":true}',});// ... run your browser test ...constverdict=awaitclient.assertSeen({upstream: "api",path_pattern: "^/orders/\\d+/refund$"},5_000,);if(!verdict.passed)thrownewError(verdict.message);awaitclient.close();Every scenario the spec describes is covered by an integration test against a real TCP socket / hyper listener — no mocked HTTP clients in the hot paths.
# Rust: fmt + clippy (with -D warnings) + build + cargo test
scripts/test-unit.sh
# TypeScript client: tsc + vitest
scripts/test-ts.shBoth scripts are the source of truth for what CI runs. The GitHub Actions
workflow in .github/workflows/ci.yml delegates
to them.
| Capability | Notes |
|---|---|
| Plain HTTP listener + forwarder (hyper 1.x) | HTTP/1.1 + HTTP/2 auto-negotiation |
| Inbound + outbound TLS (rustls) | Custom CAs and accept_invalid_certs for testing |
| Recorder + pluggable snapshot storage | NDJSON / SQLite |
Replay (MethodUriAndBodyHash + Custom) | O(1) indexed lookup; goes through redaction hooks |
Middleware chain with Next<'_> | Body rewrites, short-circuit, error recovery |
| Stubs + in-process command plane | Fire-count, artificial delay, pause/resume |
| TCP JSON-Lines control plane | Same command set, cross-language harnesses |
Wait-for AssertSeen / AssertCount | Overshoot terminates fast |
Hosting example (examples/host.rs) | Env-var-driven; ~30 lines |
| TypeScript client + vitest | Mock + real-binary e2e suites |