11package org .codejive .tproxy ;
22
3+ import java .io .ByteArrayInputStream ;
4+ import java .io .IOException ;
5+ import java .io .InputStream ;
36import java .net .URI ;
47import java .util .Objects ;
8+ import java .util .function .Supplier ;
59
610/**
711 * Immutable representation of an HTTP request in the proxy layer. Use {@code with*()} methods to
812 * create modified copies.
13+ *
14+ * <p>The body can be accessed either as a stream via {@link #bodyStream()} or as a byte array via
15+ * {@link #body()}. Bodies created from {@code InputStream} or {@code Supplier<InputStream>} are
16+ * single-use - calling {@link #bodyStream()} a second time will throw an exception. Bodies created
17+ * from byte arrays support multiple {@link #bodyStream()} calls.
18+ *
19+ * <p>The {@link #body()} method materializes the stream into a byte array and caches it, so
20+ * subsequent calls return the same cached array.
921 */
1022public class ProxyRequest {
1123 private final String method ;
1224 private final URI uri ;
1325 private final Headers headers ;
14- private final byte [] body ;
26+ private final Supplier <InputStream > bodySupplier ;
27+ private final boolean isByteArrayBased ;
28+ private final boolean isSupplierBased ;
29+ private boolean streamConsumed = false ;
30+ private byte [] cachedBody = null ;
1531
16- public ProxyRequest (String method , URI uri , Headers headers , byte [] body ) {
32+ /**
33+ * Create a request with a byte array body. The body can be read multiple times.
34+ *
35+ * @param method the HTTP method
36+ * @param uri the request URI
37+ * @param headers the request headers
38+ * @param body the request body as a byte array
39+ * @return a new ProxyRequest
40+ */
41+ public static ProxyRequest fromBytes (String method , URI uri , Headers headers , byte [] body ) {
42+ byte [] bodyBytes = body != null ? body : new byte [0 ];
43+ Supplier <InputStream > supplier = () -> new ByteArrayInputStream (bodyBytes );
44+ return new ProxyRequest (method , uri , headers , supplier , true , false , bodyBytes );
45+ }
46+
47+ /**
48+ * Create a request with a streaming body from an InputStream. The stream can only be read once.
49+ *
50+ * @param method the HTTP method
51+ * @param uri the request URI
52+ * @param headers the request headers
53+ * @param bodyStream the request body as an InputStream (single-use)
54+ * @return a new ProxyRequest
55+ */
56+ public static ProxyRequest fromStream (
57+ String method , URI uri , Headers headers , InputStream bodyStream ) {
58+ Supplier <InputStream > supplier =
59+ bodyStream != null ? () -> bodyStream : () -> new ByteArrayInputStream (new byte [0 ]);
60+ return new ProxyRequest (method , uri , headers , supplier , false , false , null );
61+ }
62+
63+ /**
64+ * Create a request with a body supplier that provides re-readable streams.
65+ *
66+ * @param method the HTTP method
67+ * @param uri the request URI
68+ * @param headers the request headers
69+ * @param bodySupplier supplier that provides a fresh InputStream on each call
70+ * @return a new ProxyRequest
71+ */
72+ public static ProxyRequest fromSupplier (
73+ String method , URI uri , Headers headers , Supplier <InputStream > bodySupplier ) {
74+ Supplier <InputStream > supplier =
75+ bodySupplier != null ? bodySupplier : () -> new ByteArrayInputStream (new byte [0 ]);
76+ return new ProxyRequest (method , uri , headers , supplier , false , true , null );
77+ }
78+
79+ /**
80+ * Main constructor used by factory methods.
81+ *
82+ * @param method the HTTP method
83+ * @param uri the request URI
84+ * @param headers the request headers
85+ * @param bodySupplier supplier that provides the request body stream
86+ * @param isByteArrayBased whether the body is backed by a byte array
87+ * @param isSupplierBased whether the body supplier can be called multiple times
88+ * @param cachedBody optional cached body bytes (for byte array-based bodies)
89+ */
90+ private ProxyRequest (
91+ String method ,
92+ URI uri ,
93+ Headers headers ,
94+ Supplier <InputStream > bodySupplier ,
95+ boolean isByteArrayBased ,
96+ boolean isSupplierBased ,
97+ byte [] cachedBody ) {
1798 this .method = Objects .requireNonNull (method , "method cannot be null" );
1899 this .uri = Objects .requireNonNull (uri , "uri cannot be null" );
19100 this .headers = headers != null ? headers : Headers .empty ();
20- this .body = body != null ? body .clone () : new byte [0 ];
101+ this .bodySupplier = bodySupplier ;
102+ this .isByteArrayBased = isByteArrayBased ;
103+ this .isSupplierBased = isSupplierBased ;
104+ this .cachedBody = cachedBody ;
105+ }
106+
107+ /** Copy constructor for wither methods. */
108+ private ProxyRequest (
109+ String method ,
110+ URI uri ,
111+ Headers headers ,
112+ Supplier <InputStream > bodySupplier ,
113+ boolean isByteArrayBased ,
114+ boolean isSupplierBased ,
115+ boolean streamConsumed ,
116+ byte [] cachedBody ) {
117+ this .method = method ;
118+ this .uri = uri ;
119+ this .headers = headers ;
120+ this .bodySupplier = bodySupplier ;
121+ this .isByteArrayBased = isByteArrayBased ;
122+ this .isSupplierBased = isSupplierBased ;
123+ this .streamConsumed = streamConsumed ;
124+ this .cachedBody = cachedBody ;
21125 }
22126
23127 public String method () {
@@ -32,28 +136,142 @@ public Headers headers() {
32136 return headers ;
33137 }
34138
139+ /**
140+ * Get the request body as an InputStream.
141+ *
142+ * <p>For byte array-based bodies, this method can be called multiple times and returns a new
143+ * {@code ByteArrayInputStream} on each call.
144+ *
145+ * <p>For supplier-based bodies, this method can be called multiple times and the supplier
146+ * provides a fresh InputStream on each call.
147+ *
148+ * <p>For stream-based bodies (created from {@code InputStream}), this method can only be called
149+ * once unless {@link #body()} has been called to materialize it. Subsequent calls will throw
150+ * {@code IllegalStateException}.
151+ *
152+ * @return an InputStream containing the request body
153+ * @throws IllegalStateException if called more than once on a stream-based body
154+ */
155+ public InputStream bodyStream () {
156+ // If body has been materialized, always return from cached
157+ if (cachedBody != null ) {
158+ return new ByteArrayInputStream (cachedBody );
159+ }
160+
161+ // Supplier-based bodies are repeatable
162+ if (isSupplierBased ) {
163+ return bodySupplier .get ();
164+ }
165+
166+ // Stream-based bodies are single-use
167+ if (!isByteArrayBased && streamConsumed ) {
168+ throw new IllegalStateException (
169+ "Body stream already consumed. Create a new ProxyRequest with a re-readable"
170+ + " body source." );
171+ }
172+ if (!isByteArrayBased ) {
173+ streamConsumed = true ;
174+ }
175+ return bodySupplier .get ();
176+ }
177+
178+ /**
179+ * Get the request body as a byte array. This method materializes the stream if not already
180+ * cached, and returns the cached result on subsequent calls.
181+ *
182+ * <p>After calling this method, the request becomes byte-array-based, allowing multiple
183+ * bodyStream() calls.
184+ *
185+ * @return the request body as a byte array
186+ * @throws RuntimeException if an I/O error occurs reading the stream
187+ */
35188 public byte [] body () {
36- return body .clone ();
189+ if (cachedBody == null ) {
190+ try {
191+ cachedBody = bodyStream ().readAllBytes ();
192+ } catch (IOException e ) {
193+ throw new RuntimeException ("Failed to read request body" , e );
194+ }
195+ }
196+ return cachedBody ;
37197 }
38198
39199 public ProxyRequest withMethod (String method ) {
40- return new ProxyRequest (method , uri , headers , body );
200+ return new ProxyRequest (
201+ method ,
202+ uri ,
203+ headers ,
204+ bodySupplier ,
205+ isByteArrayBased ,
206+ isSupplierBased ,
207+ streamConsumed ,
208+ cachedBody );
41209 }
42210
43211 public ProxyRequest withUri (URI uri ) {
44- return new ProxyRequest (method , uri , headers , body );
212+ return new ProxyRequest (
213+ method ,
214+ uri ,
215+ headers ,
216+ bodySupplier ,
217+ isByteArrayBased ,
218+ isSupplierBased ,
219+ streamConsumed ,
220+ cachedBody );
45221 }
46222
47223 public ProxyRequest withHeaders (Headers headers ) {
48- return new ProxyRequest (method , uri , headers , body );
224+ return new ProxyRequest (
225+ method ,
226+ uri ,
227+ headers ,
228+ bodySupplier ,
229+ isByteArrayBased ,
230+ isSupplierBased ,
231+ streamConsumed ,
232+ cachedBody );
49233 }
50234
51235 public ProxyRequest withHeader (String name , String value ) {
52- return new ProxyRequest (method , uri , headers .with (name , value ), body );
236+ return new ProxyRequest (
237+ method ,
238+ uri ,
239+ headers .with (name , value ),
240+ bodySupplier ,
241+ isByteArrayBased ,
242+ isSupplierBased ,
243+ streamConsumed ,
244+ cachedBody );
53245 }
54246
247+ /**
248+ * Create a new request with a byte array body.
249+ *
250+ * @param body the new body
251+ * @return a new ProxyRequest
252+ */
55253 public ProxyRequest withBody (byte [] body ) {
56- return new ProxyRequest (method , uri , headers , body );
254+ return ProxyRequest .fromBytes (method , uri , headers , body );
255+ }
256+
257+ /**
258+ * Create a new request with a streaming body.
259+ *
260+ * @param bodyStream the new body stream
261+ * @return a new ProxyRequest
262+ */
263+ public ProxyRequest withBody (InputStream bodyStream ) {
264+ return ProxyRequest .fromStream (method , uri , headers , bodyStream );
265+ }
266+
267+ /**
268+ * Create a new request with a body supplier.
269+ *
270+ * @param bodySupplier supplier that provides a fresh InputStream on each call
271+ * @return a new ProxyRequest
272+ */
273+ public ProxyRequest withBody (Supplier <InputStream > bodySupplier ) {
274+ return ProxyRequest .fromSupplier (method , uri , headers , bodySupplier );
57275 }
58276
59277 @ Override
@@ -64,13 +282,13 @@ public boolean equals(Object o) {
64282 return Objects .equals (method , that .method )
65283 && Objects .equals (uri , that .uri )
66284 && Objects .equals (headers , that .headers )
67- && java .util .Arrays .equals (body , that .body );
285+ && java .util .Arrays .equals (body () , that .body () );
68286 }
69287
70288 @ Override
71289 public int hashCode () {
72290 int result = Objects .hash (method , uri , headers );
73- result = 31 * result + java .util .Arrays .hashCode (body );
291+ result = 31 * result + java .util .Arrays .hashCode (body () );
74292 return result ;
75293 }
76294
@@ -85,7 +303,7 @@ public String toString() {
85303 + ", headers="
86304 + headers
87305 + ", bodyLength="
88- + body .length
306+ + ( cachedBody != null ? cachedBody .length : "stream" )
89307 + '}' ;
90308 }
91309}
0 commit comments