A library for recording and replaying web traffic for testing purposes. Built on top of java-tproxy, it provides a simple way to record HTTP/HTTPS traffic and replay it later for deterministic testing.
- Three operational modes: Record, Cache, and Replay
- HTTP/HTTPS support: Automatic HTTPS interception for recording encrypted traffic
- Pluggable storage: In-memory or file-based persistence
- Flexible request matching: Configurable matching strategies (method + URI, headers, body)
- Custom file naming: Pluggable naming strategy for stored recordings
- Thread-safe: Handles concurrent requests safely
- Minimal dependencies: Lightweight with only essential dependencies
- Java 21+: Modern Java features and APIs
- RECORD: All traffic passes through to the actual server and is recorded. Perfect for creating test fixtures from real API responses.
- CACHE: Returns cached responses when available, otherwise passes through and records. Ideal for speeding up tests while allowing new endpoints to be recorded.
- REPLAY: Only returns cached responses; non-matching requests return 404. Great for fully isolated tests that don't require network access.
Add as a dependency to your project:
<dependency>
<groupId>org.codejive.webreplay</groupId>
<artifactId>java-webreplay</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>You can run the proxy as a standalone application without writing any code:
# Run with defaults (CACHE mode on port 3128, storing to "proxy-cache")
java -cp target/java-webreplay-0.0.1-SNAPSHOT.jar org.codejive.webreplay.MainAvailable options:
-p, --port <port>- Port to run the proxy on (default: 3128)-d, --dir <directory>- Directory for storing cached requests (default: proxy-cache)-m, --mode <mode>- Replay mode: RECORD, CACHE, or REPLAY (default: CACHE)-h, --help- Show help message
Once running, configure your browser or application to use localhost:<port> as the HTTP/HTTPS proxy.
importorg.codejive.webreplay.WebReplayProxy;
importorg.codejive.webreplay.ReplayMode;
importjava.nio.file.Path;
// Start in RECORD mode to capture trafficWebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("recordings"))
.build();
proxy.start(8080);
// Configure your HTTP client to use the proxy on port 8080// Make requests to your API - they will be recorded// Later, switch to REPLAY mode for testingproxy.setMode(ReplayMode.REPLAY);
// Now requests will use cached responses without hitting the real serverWebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.inMemoryStorage() // No disk persistence
.build();
proxy.start(8080);importorg.codejive.webreplay.WebReplayProxy;
importorg.codejive.webreplay.ReplayMode;
importjava.nio.file.Path;
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("test/recordings"))
.build();
proxy.start(8080);
// Configure your application or test to use proxy// All HTTP traffic will be recorded to disk// When doneproxy.stop();Recordings are saved as JSON files with names like:
example.com_443_GET_api-users_a3f8b9.json
api.service.com_80_POST_data_create_7e2f45.json
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.storageDirectory(Path.of("test/recordings"))
.build();
proxy.start(8080);
// First request: cache miss -> hits real server and records// makeRequest("http://api.example.com/users");// Second request: cache hit -> returns cached response instantly// makeRequest("http://api.example.com/users");// Perfect for CI/CD environments with no external network accessWebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.REPLAY)
.storageDirectory(Path.of("test/recordings"))
.build();
proxy.start(8080);
// Only cached requests succeed// Non-cached requests return 404 "Recording not found"WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("recordings"))
.build();
proxy.start(8080);
// Record some traffic// ...// Switch to replay modeproxy.setMode(ReplayMode.REPLAY);
// Now uses cached responses onlyBy default, requests are matched on method + URI only. You can customize this:
importorg.codejive.webreplay.matching.DefaultRequestMatcher;
// Match requests including specific headersRequestMatchermatcher = DefaultRequestMatcher.builder()
.matchHeader("Authorization")
.matchHeader("Content-Type")
.build();
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.matcher(matcher)
.storageDirectory(Path.of("recordings"))
.build();Match request bodies:
RequestMatchermatcher = DefaultRequestMatcher.builder()
.matchBody() // Include body in matching
.build();Create a custom matcher:
RequestMatchercustomMatcher = (request1, request2) -> {
// Your custom matching logicreturnrequest1.method().equals(request2.method())
&& request1.uri().getPath().equals(request2.uri().getPath());
};
WebReplayProxyproxy = WebReplayProxy.builder()
.matcher(customMatcher)
.storageDirectory(Path.of("recordings"))
.build();importorg.codejive.webreplay.storage.FileNamingStrategy;
// Create a custom naming strategyFileNamingStrategycustomNaming = request -> {
Stringmethod = request.method();
Stringpath = request.uri().getPath().replace("/", "_");
returnString.format("%s%s.json", method, path);
};
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("recordings"))
.namingStrategy(customNaming)
.build();importorg.codejive.webreplay.storage.RequestResponseStore;
// Implement your own storage (e.g., database, Redis, S3)RequestResponseStorecustomStore = newMyDatabaseStore();
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.store(customStore)
.build();importjava.nio.file.Path;
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("recordings"))
.caStorageDirectory(Path.of("certs")) // Custom CA cert location
.build();The proxy automatically generates a CA certificate for HTTPS interception. Import tproxy-ca.crt into your browser or trust store to avoid certificate warnings.
importorg.junit.jupiter.api.*;
importjava.nio.file.Path;
classMyApiTest {
privateWebReplayProxyproxy;
@BeforeAllstaticvoidrecordFixtures() throwsException {
// One-time recording of test fixturesWebReplayProxyrecorder = WebReplayProxy.builder()
.mode(ReplayMode.RECORD)
.storageDirectory(Path.of("src/test/resources/recordings"))
.build();
recorder.start(8080);
// Make real API calls to record// ...recorder.stop();
}
@BeforeEachvoidsetUp() throwsException {
proxy = WebReplayProxy.builder()
.mode(ReplayMode.REPLAY)
.storageDirectory(Path.of("src/test/resources/recordings"))
.build();
proxy.start(8080);
}
@AfterEachvoidtearDown() {
proxy.stop();
}
@TestvoidtestWithReplayedResponses() {
// Your tests here - all HTTP calls use cached responses// No actual network requests are made
}
}@TestvoidtestWithTemporaryRecordings() throwsException {
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.inMemoryStorage() // Temporary recordings
.build();
proxy.start(8080);
// Make requests - they're cached in memory// ...// Verify recording countassertThat(proxy.getRecordingCount()).isGreaterThan(0);
// Clear recordingsproxy.clearRecordings();
assertThat(proxy.getRecordingCount()).isZero();
proxy.stop();
}importjava.net.http.HttpClient;
importjava.net.http.HttpRequest;
importjava.net.http.HttpResponse;
importjava.net.URI;
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.storageDirectory(Path.of("recordings"))
.build();
proxy.start(8080);
// Option 1: Configure a specific HttpClientHttpClientclient = HttpClient.newBuilder()
.proxy(proxy.asProxySelector())
.build();
// Option 2: Set as system default for all HttpClient instancesjava.net.ProxySelector.setDefault(proxy.asProxySelector());
HttpClientclient = HttpClient.newHttpClient(); // Uses system defaultHttpRequestrequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString());importorg.codejive.tproxy.ProxyRequest;
importorg.codejive.tproxy.ProxyResponse;
importorg.codejive.tproxy.Headers;
WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.REPLAY)
.storageDirectory(Path.of("recordings"))
.build();
proxy.start(8080);
// Execute requests directly through the proxy APIProxyRequestrequest = ProxyRequest.fromBytes(
"GET",
URI.create("http://api.example.com/users"),
Headers.of("Accept", "application/json"),
null
);
ProxyResponseresponse = proxy.getHttpProxy().execute(request);
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + newString(response.body()));| Method | Description |
|---|---|
mode(ReplayMode) | Set the operational mode (RECORD, CACHE, REPLAY) |
storageDirectory(Path) | Use file-based storage at the specified directory |
inMemoryStorage() | Use in-memory storage (non-persistent) |
store(RequestResponseStore) | Use a custom storage implementation |
matcher(RequestMatcher) | Use a custom request matching strategy |
namingStrategy(FileNamingStrategy) | Use a custom file naming strategy |
caStorageDirectory(Path) | Set directory for CA certificate storage |
build() | Build the WebReplayProxy instance |
| Method | Description |
|---|---|
start(int port) | Start the proxy on the specified port |
stop() | Stop the proxy |
setMode(ReplayMode) | Change the operational mode |
getMode() | Get the current mode |
clearRecordings() | Clear all recorded exchanges |
getRecordingCount() | Get the number of recorded exchanges |
getHttpProxy() | Get the underlying HttpProxy instance |
getStore() | Get the storage instance |
RECORD- Pass through and record all trafficCACHE- Return cached responses when available, otherwise pass through and recordREPLAY- Return only cached responses, 404 for non-cached
Recordings are stored as JSON files with the following structure:
{
"request": {
"method": "GET",
"uri": "https://api.example.com/users/123",
"headers": {
"Accept": ["application/json"],
"User-Agent": ["MyApp/1.0"]
},
"body": "base64-encoded-body"
},
"response": {
"statusCode": 200,
"headers": {
"Content-Type": ["application/json"],
"Cache-Control": ["no-cache"]
},
"body": "base64-encoded-body"
},
"recordedAt": {
"epochSecond": 1712743200,
"nano": 123456789
}
}Build the project:
./mvnw clean installRun tests:
./mvnw testFormat code:
./mvnw spotless:apply- Java 21 or higher
- Maven 3.6 or higher
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.