Skip to content

Repository files navigation

Java Web Replay

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.

Features

  • 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

Modes

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

Installation

Add as a dependency to your project:

<dependency>
<groupId>org.codejive.webreplay</groupId>
<artifactId>java-webreplay</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

Running as Standalone Proxy

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

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

Quick Start

Basic Recording and Replay

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 server

In-Memory Storage (for testing)

WebReplayProxyproxy = WebReplayProxy.builder()
.mode(ReplayMode.CACHE)
.inMemoryStorage() // No disk persistence
.build();
proxy.start(8080);

Usage Examples

RECORD Mode: Capture Real API Responses

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

CACHE Mode: Speed Up Tests

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");

REPLAY Mode: Fully Isolated Tests

// 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"

Dynamic Mode Switching

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 only

Advanced Configuration

Custom Request Matching

By 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();

Custom File Naming Strategy

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();

Custom Storage Implementation

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();

HTTPS Certificate Management

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.

Integration with Testing Frameworks

JUnit 5 Example

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

Testing with In-Memory Storage

@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();
}

Working with the Proxy

Using with HttpClient

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());

Direct Execution (without HttpClient proxy configuration)

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()));

API Reference

WebReplayProxy.Builder

MethodDescription
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

WebReplayProxy Methods

MethodDescription
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

ReplayMode Enum

  • RECORD - Pass through and record all traffic
  • CACHE - Return cached responses when available, otherwise pass through and record
  • REPLAY - Return only cached responses, 404 for non-cached

Storage Format

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

Building

Build the project:

./mvnw clean install

Run tests:

./mvnw test

Format code:

./mvnw spotless:apply

Requirements

  • Java 21 or higher
  • Maven 3.6 or higher

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

About

A library for recording and replaying web traffic for testing purposes

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages