Skip to content

Commit 92a9989

Browse files
committed
chore: added ProxyResponse.materializeTo() utility method
1 parent d8440f8 commit 92a9989

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

src/main/java/org/codejive/tproxy/ProxyResponse.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
55
import java.io.InputStream;
6+
import java.io.UncheckedIOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.StandardCopyOption;
610
import java.util.Objects;
711
import java.util.function.Supplier;
812

@@ -18,14 +22,13 @@
1822
* <p>The {@link #body()} method materializes the stream into a byte array and caches it, so
1923
* subsequent calls return the same cached array.
2024
*
21-
* <p><b>Important for Interceptors:</b> If your interceptor needs to read the body, you must create
22-
* a new {@code ProxyResponse} with a re-readable body source. For example:
25+
* <p><b>Important for Interceptors:</b> If your interceptor needs to read the body, you can use
26+
* {@link #materializeTo(Path)} to save it to a file and get a re-readable response:
2327
*
2428
* <pre>{@code
2529
* ProxyResponse original = chain.proceed(request);
2630
* Path tempFile = Files.createTempFile("proxy", ".tmp");
27-
* Files.copy(original.bodyStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
28-
* return original.withBody(() -> Files.newInputStream(tempFile));
31+
* return original.materializeTo(tempFile);
2932
* }</pre>
3033
*/
3134
public class ProxyResponse {
@@ -256,6 +259,33 @@ public ProxyResponse withBody(Supplier<InputStream> bodySupplier) {
256259
return ProxyResponse.fromSupplier(statusCode, headers, bodySupplier);
257260
}
258261

262+
/**
263+
* Materialize the response body to a file and return a new ProxyResponse with a re-readable
264+
* body source that reads from the file. This is useful for interceptors that need to read the
265+
* body multiple times.
266+
*
267+
* <p>The body stream is copied to the specified file, replacing it if it already exists. The
268+
* returned ProxyResponse will have a supplier-based body that opens a new InputStream from the
269+
* file on each call to {@link #bodyStream()}.
270+
*
271+
* @param path the file path to write the body to
272+
* @return a new ProxyResponse with a supplier that reads from the file
273+
* @throws IOException if an I/O error occurs while writing to the file
274+
*/
275+
public ProxyResponse materializeTo(Path path) throws IOException {
276+
try (InputStream bodyStream = bodyStream()) {
277+
Files.copy(bodyStream, path, StandardCopyOption.REPLACE_EXISTING);
278+
}
279+
return withBody(
280+
() -> {
281+
try {
282+
return Files.newInputStream(path);
283+
} catch (IOException e) {
284+
throw new UncheckedIOException("Failed to read from " + path, e);
285+
}
286+
});
287+
}
288+
259289
@Override
260290
public boolean equals(Object o) {
261291
if (this == o) return true;

src/test/java/org/codejive/tproxy/StreamingProxyTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import java.io.IOException;
88
import java.io.InputStream;
99
import java.net.URI;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
1012
import java.util.Random;
1113
import org.junit.jupiter.api.DisplayName;
1214
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.io.TempDir;
1316

1417
/** Tests for streaming body support in ProxyRequest and ProxyResponse. */
1518
@DisplayName("Streaming Proxy Tests")
@@ -167,6 +170,53 @@ public void testSupplierBasedBody() throws IOException {
167170
assertThat(content2).isEqualTo("repeatable data");
168171
}
169172

173+
@Test
174+
@DisplayName("materializeTo() saves body to file and returns re-readable response")
175+
public void testMaterializeTo(@TempDir Path tempDir) throws IOException {
176+
// Create a response with a single-use stream
177+
InputStream stream = new ByteArrayInputStream("test content for file".getBytes());
178+
ProxyResponse response = ProxyResponse.fromStream(200, Headers.empty(), stream);
179+
180+
// Materialize to a temp file
181+
Path tempFile = tempDir.resolve("response.bin");
182+
ProxyResponse materialized = response.materializeTo(tempFile);
183+
184+
// Verify file was created and contains the body
185+
assertThat(tempFile).exists();
186+
String fileContent = Files.readString(tempFile);
187+
assertThat(fileContent).isEqualTo("test content for file");
188+
189+
// Verify the materialized response is re-readable
190+
String content1 = new String(materialized.bodyStream().readAllBytes());
191+
assertThat(content1).isEqualTo("test content for file");
192+
193+
String content2 = new String(materialized.bodyStream().readAllBytes());
194+
assertThat(content2).isEqualTo("test content for file");
195+
196+
// Verify status code and headers are preserved
197+
assertThat(materialized.statusCode()).isEqualTo(200);
198+
assertThat(materialized.headers()).isEqualTo(Headers.empty());
199+
}
200+
201+
@Test
202+
@DisplayName("materializeTo() replaces existing file")
203+
public void testMaterializeToReplacesFile(@TempDir Path tempDir) throws IOException {
204+
Path tempFile = tempDir.resolve("response.bin");
205+
206+
// Create a file with existing content
207+
Files.writeString(tempFile, "old content");
208+
assertThat(Files.readString(tempFile)).isEqualTo("old content");
209+
210+
// Materialize new content to the same file
211+
ProxyResponse response =
212+
ProxyResponse.fromBytes(200, Headers.empty(), "new content".getBytes());
213+
ProxyResponse materialized = response.materializeTo(tempFile);
214+
215+
// Verify file was replaced
216+
assertThat(Files.readString(tempFile)).isEqualTo("new content");
217+
assertThat(new String(materialized.bodyStream().readAllBytes())).isEqualTo("new content");
218+
}
219+
170220
/**
171221
* Virtual InputStream that generates data on-the-fly without allocating large byte arrays. This
172222
* simulates a large response body for testing streaming without using excessive memory.

0 commit comments

Comments
 (0)