You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2026/07/26 16:53:19 UTC

(httpcomponents-client) branch master updated: Extended HttpCacheEntry and related classes to store the enclosed request body (#859)

This is an automated email from the ASF dual-hosted git repository.

ok2c pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


The following commit(s) were added to refs/heads/master by this push:
     new c4eb7e5a4 Extended HttpCacheEntry and related classes to store the enclosed request body (#859)
c4eb7e5a4 is described below

commit c4eb7e5a4fa095c334391138d0462c9674d99984
Author: hunghhdev <hu...@gmail.com>
AuthorDate: Sun Jul 26 23:53:14 2026 +0700

    Extended HttpCacheEntry and related classes to store the enclosed request body (#859)
---
 .../hc/client5/http/cache/HttpCacheEntry.java      |  34 +++++++
 .../client5/http/cache/HttpCacheEntryFactory.java  |  27 ++++++
 .../http/impl/cache/BasicHttpAsyncCache.java       |   6 +-
 .../hc/client5/http/impl/cache/BasicHttpCache.java |   6 +-
 .../cache/HttpByteArrayCacheEntrySerializer.java   |  66 +++++++++----
 .../http/cache/TestHttpCacheEntryFactory.java      |  18 ++++
 .../http/impl/cache/HttpCacheEntryMatcher.java     |   3 +
 .../http/impl/cache/TestBasicHttpAsyncCache.java   |  21 +++++
 .../http/impl/cache/TestBasicHttpCache.java        |  14 +++
 .../TestHttpByteArrayCacheEntrySerializer.java     | 105 +++++++++++++++++++++
 10 files changed, 276 insertions(+), 24 deletions(-)

diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntry.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntry.java
index 5f7f10ee1..d362a379b 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntry.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntry.java
@@ -73,6 +73,7 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
     private final String method;
     private final String requestURI;
     private final HeaderGroup requestHeaders;
+    private final byte[] requestContent;
     private final int status;
     private final HeaderGroup responseHeaders;
     private final Resource resource;
@@ -86,6 +87,8 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
     /**
      * Internal constructor that makes no validation of the input parameters and makes
      * no copies of the original client request and the origin response.
+     *
+     * @since 5.7
      */
     @Internal
     public HttpCacheEntry(
@@ -94,6 +97,7 @@ public HttpCacheEntry(
             final String method,
             final String requestURI,
             final HeaderGroup requestHeaders,
+            final byte[] requestContent,
             final int status,
             final HeaderGroup responseHeaders,
             final Resource resource,
@@ -104,6 +108,7 @@ public HttpCacheEntry(
         this.method = method;
         this.requestURI = requestURI;
         this.requestHeaders = requestHeaders;
+        this.requestContent = requestContent;
         this.status = status;
         this.responseHeaders = responseHeaders;
         this.resource = resource;
@@ -114,6 +119,24 @@ public HttpCacheEntry(
         this.eTagRef = new AtomicReference<>();
     }
 
+    /**
+     * Internal constructor that makes no validation of the input parameters and makes
+     * no copies of the original client request and the origin response.
+     */
+    @Internal
+    public HttpCacheEntry(
+            final Instant requestDate,
+            final Instant responseDate,
+            final String method,
+            final String requestURI,
+            final HeaderGroup requestHeaders,
+            final int status,
+            final HeaderGroup responseHeaders,
+            final Resource resource,
+            final Collection<String> variants) {
+        this(requestDate, responseDate, method, requestURI, requestHeaders, null, status, responseHeaders, resource, variants);
+    }
+
     /**
      * Create a new {@link HttpCacheEntry} with variants.
      * @param requestDate
@@ -176,6 +199,7 @@ public HttpCacheEntry(
         this.method = Method.GET.name();
         this.requestURI = "/";
         this.requestHeaders = new HeaderGroup();
+        this.requestContent = null;
         this.status = status;
         this.responseHeaders = new HeaderGroup();
         this.responseHeaders.setHeaders(responseHeaders);
@@ -491,6 +515,16 @@ public Iterator<Header> requestHeaderIterator(final String headerName) {
         return requestHeaders.headerIterator(headerName);
     }
 
+    /**
+     * Returns the content of the original client request or {@code null} if the request
+     * did not enclose any content.
+     *
+     * @since 5.7
+     */
+    public byte[] getRequestContent() {
+        return requestContent;
+    }
+
     /**
      * Tests if the given {@link HttpCacheEntry} is newer than the given {@link MessageHeaders}
      * by comparing values of their {@literal DATE} header. In case the given entry, or the message,
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntryFactory.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntryFactory.java
index 9b86f0bcc..03c01f0a1 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntryFactory.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntryFactory.java
@@ -130,6 +130,7 @@ public HttpCacheEntry createRoot(final HttpCacheEntry latestVariant,
                 latestVariant.getRequestMethod(),
                 latestVariant.getRequestURI(),
                 headers(latestVariant.requestHeaderIterator()),
+                latestVariant.getRequestContent(),
                 latestVariant.getStatus(),
                 headers(latestVariant.headerIterator()),
                 null,
@@ -152,6 +153,29 @@ public HttpCacheEntry create(final Instant requestInstant,
                                  final HttpRequest request,
                                  final HttpResponse response,
                                  final Resource resource) {
+        return create(requestInstant, responseInstant, host, request, null, response, resource);
+    }
+
+    /**
+     * Create a new {@link HttpCacheEntry} with the given request content and {@link Resource}.
+     *
+     * @param requestInstant   Date/time when the request was made (Used for age calculations)
+     * @param responseInstant  Date/time that the response came back (Used for age calculations)
+     * @param host             Target host
+     * @param request          Original client request (a deep copy of this object is made)
+     * @param requestContent   Content enclosed in the original client request or {@code null}
+     * @param response         Origin response (a deep copy of this object is made)
+     * @param resource         Resource representing origin response body
+     *
+     * @since 5.7
+     */
+    public HttpCacheEntry create(final Instant requestInstant,
+                                 final Instant responseInstant,
+                                 final HttpHost host,
+                                 final HttpRequest request,
+                                 final byte[] requestContent,
+                                 final HttpResponse response,
+                                 final Resource resource) {
         Args.notNull(requestInstant, "Request instant");
         Args.notNull(responseInstant, "Response instant");
         Args.notNull(host, "Host");
@@ -169,6 +193,7 @@ public HttpCacheEntry create(final Instant requestInstant,
                 request.getMethod(),
                 uri,
                 requestHeaders,
+                requestContent,
                 response.getCode(),
                 responseHeaders,
                 resource,
@@ -213,6 +238,7 @@ public HttpCacheEntry createUpdated(
                 request.getMethod(),
                 uri,
                 requestHeaders,
+                entry.getRequestContent(),
                 entry.getStatus(),
                 mergedHeaders,
                 entry.getResource(),
@@ -233,6 +259,7 @@ public HttpCacheEntry copy(final HttpCacheEntry entry) {
                 entry.getRequestMethod(),
                 entry.getRequestURI(),
                 headers(entry.requestHeaderIterator()),
+                entry.getRequestContent(),
                 entry.getStatus(),
                 headers(entry.headerIterator()),
                 entry.getResource(),
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java
index 658e2754d..fd2887c51 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java
@@ -390,7 +390,8 @@ public Cancellable store(
             final Instant requestSent,
             final Instant responseReceived,
             final FutureCallback<CacheHit> callback) {
-        final String rootKey = cacheKeyGenerator.generateKey(host, request, SimpleHttpRequest::getBodyBytes);
+        final byte[] requestContent = request.getBodyBytes();
+        final String rootKey = cacheKeyGenerator.generateKey(host, request, r -> requestContent);
         if (LOG.isDebugEnabled()) {
             LOG.debug("Create cache entry: {}", rootKey);
         }
@@ -410,12 +411,13 @@ public Cancellable store(
                     responseReceived,
                     host,
                     request,
+                    requestContent,
                     originResponse,
                     content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
             callback.completed(new CacheHit(rootKey, backup));
             return Operations.nonCancellable();
         }
-        final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, originResponse, resource);
+        final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, requestContent, originResponse, resource);
         final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
         return store(rootKey,variantKey, entry, callback);
     }
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java
index 2ab2f1656..3f112f155 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java
@@ -223,7 +223,8 @@ public CacheHit store(
             final ByteArrayBuffer content,
             final Instant requestSent,
             final Instant responseReceived) {
-        final String rootKey = cacheKeyGenerator.generateKey(host, request, SimpleHttpRequest::getBodyBytes);
+        final byte[] requestContent = request.getBodyBytes();
+        final String rootKey = cacheKeyGenerator.generateKey(host, request, r -> requestContent);
         if (LOG.isDebugEnabled()) {
             LOG.debug("Create cache entry: {}", rootKey);
         }
@@ -243,11 +244,12 @@ public CacheHit store(
                     responseReceived,
                     host,
                     request,
+                    requestContent,
                     originResponse,
                     content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
             return new CacheHit(rootKey, backup);
         }
-        final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, originResponse, resource);
+        final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, requestContent, originResponse, resource);
         final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
         return store(rootKey,variantKey, entry);
     }
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java
index be0785e60..42dc952fc 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java
@@ -86,6 +86,7 @@ public class HttpByteArrayCacheEntrySerializer implements HttpCacheEntrySerializ
 
     static final String HC_CACHE_KEY = "HC-Key";
     static final String HC_CACHE_LENGTH = "HC-Resource-Length";
+    static final String HC_REQUEST_CONTENT_LENGTH = "HC-Request-Content-Length";
     static final String HC_REQUEST_INSTANT = "HC-Request-Instant";
     static final String HC_RESPONSE_INSTANT = "HC-Response-Instant";
     static final String HC_VARIANT = "HC-Variant";
@@ -133,8 +134,10 @@ public byte[] serialize(final HttpCacheStorageEntry storageEntry) throws Resourc
         final String key = storageEntry.getKey();
         final HttpCacheEntry cacheEntry = storageEntry.getContent();
         final Resource resource = cacheEntry.getResource();
+        final byte[] requestContent = cacheEntry.getRequestContent();
 
-        try (ByteArrayOutputStream out = new ByteArrayOutputStream((resource != null ? (int) resource.length() : 0) + DEFAULT_BUFFER_SIZE)) {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream((resource != null ? (int) resource.length() : 0)
+                + (requestContent != null ? requestContent.length : 0) + DEFAULT_BUFFER_SIZE)) {
             final SessionOutputBuffer outputBuffer = new SessionOutputBufferImpl(bufferSize);
             final CharArrayBuffer line = new CharArrayBuffer(DEFAULT_BUFFER_SIZE);
 
@@ -155,6 +158,14 @@ public byte[] serialize(final HttpCacheStorageEntry storageEntry) throws Resourc
                 outputBuffer.writeLine(line, out);
             }
 
+            if (requestContent != null) {
+                line.clear();
+                line.append(HC_REQUEST_CONTENT_LENGTH);
+                line.append(": ");
+                line.append(asStr(requestContent.length));
+                outputBuffer.writeLine(line, out);
+            }
+
             line.clear();
             line.append(HC_REQUEST_INSTANT);
             line.append(": ");
@@ -189,6 +200,11 @@ public byte[] serialize(final HttpCacheStorageEntry storageEntry) throws Resourc
             line.clear();
             outputBuffer.writeLine(line, out);
 
+            if (requestContent != null) {
+                outputBuffer.flush(out);
+                out.write(requestContent);
+            }
+
             line.clear();
             final StatusLine statusLine = new StatusLine(HttpVersion.HTTP_1_1, cacheEntry.getStatus(), "");
             lineFormatter.formatStatusLine(line, statusLine);
@@ -241,6 +257,7 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
             }
             String storageKey = null;
             long length = -1;
+            long requestContentLength = -1;
             Instant requestDate = null;
             Instant responseDate = null;
             final Set<String> variants = new HashSet<>();
@@ -258,6 +275,8 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
                     storageKey = value;
                 } else if (name.equalsIgnoreCase(HC_CACHE_LENGTH)) {
                     length = asLong(value);
+                } else if (name.equalsIgnoreCase(HC_REQUEST_CONTENT_LENGTH)) {
+                    requestContentLength = asLong(value);
                 } else if (name.equalsIgnoreCase(HC_REQUEST_INSTANT)) {
                     requestDate = asInstant(value);
                 } else if (name.equalsIgnoreCase(HC_RESPONSE_INSTANT)) {
@@ -285,6 +304,8 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
                 }
                 requestHeaders.addHeader(lineParser.parseHeader(line));
             }
+            final byte[] requestContent = requestContentLength != -1 ?
+                    readBytes(inputBuffer, in, requestContentLength, serializedObject.length) : null;
             line.clear();
             checkReadResult(inputBuffer.readLine(line, in));
             final StatusLine statusLine = lineParser.parseStatusLine(line);
@@ -298,25 +319,8 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
                 responseHeaders.addHeader(lineParser.parseHeader(line));
             }
 
-            final Resource resource;
-            if (length != -1) {
-                int off = 0;
-                int remaining = (int) length;
-                final byte[] buf = new byte[remaining];
-                while (remaining > 0) {
-                    final int i = inputBuffer.read(buf, off, remaining, in);
-                    if (i > 0) {
-                        off += i;
-                        remaining -= i;
-                    }
-                    if (i == -1) {
-                        throw new ResourceIOException("Unexpected end of cache content");
-                    }
-                }
-                resource = new HeapResource(buf);
-            } else {
-                resource = null;
-            }
+            final Resource resource = length != -1 ?
+                    new HeapResource(readBytes(inputBuffer, in, length, serializedObject.length)) : null;
             if (inputBuffer.read(in) != -1) {
                 throw new ResourceIOException("Unexpected content at the end of cache content");
             }
@@ -327,6 +331,7 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
                     requestLine.getMethod(),
                     requestLine.getUri(),
                     requestHeaders,
+                    requestContent,
                     statusLine.getStatusCode(),
                     responseHeaders,
                     resource,
@@ -347,6 +352,27 @@ public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws R
         }
     }
 
+    private static byte[] readBytes(final SessionInputBufferImpl inputBuffer, final InputStream in,
+                                    final long length, final int limit) throws IOException {
+        if (length < 0 || length > limit) {
+            throw new ResourceIOException("Invalid cache content length: " + length);
+        }
+        int off = 0;
+        int remaining = (int) length;
+        final byte[] buf = new byte[remaining];
+        while (remaining > 0) {
+            final int i = inputBuffer.read(buf, off, remaining, in);
+            if (i > 0) {
+                off += i;
+                remaining -= i;
+            }
+            if (i == -1) {
+                throw new ResourceIOException("Unexpected end of cache content");
+            }
+        }
+        return buf;
+    }
+
     private static String asStr(final long value) {
         return Long.toString(value);
     }
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/TestHttpCacheEntryFactory.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/TestHttpCacheEntryFactory.java
index 8a0f277cc..09673f2df 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/TestHttpCacheEntryFactory.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/TestHttpCacheEntryFactory.java
@@ -26,6 +26,7 @@
  */
 package org.apache.hc.client5.http.cache;
 
+import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.HashSet;
 import java.util.Set;
@@ -280,6 +281,23 @@ void testCreateResourceEntry() {
         Assertions.assertFalse(newEntry.hasVariants());
     }
 
+    @Test
+    void testCreateEntryWithRequestContent() {
+        final byte[] requestContent = "{\"criteria\":\"value\"}".getBytes(StandardCharsets.UTF_8);
+        final Resource resource = HttpTestUtils.makeRandomResource(128);
+        final HttpRequest queryRequest = new BasicHttpRequest("QUERY", "/stuff");
+        final HttpResponse okResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
+
+        final HttpCacheEntry newEntry = impl.create(tenSecondsAgo, oneSecondAgo, host, queryRequest, requestContent, okResponse, resource);
+        Assertions.assertArrayEquals(requestContent, newEntry.getRequestContent());
+
+        final HttpCacheEntry updatedEntry = impl.createUpdated(oneSecondAgo, now, host, queryRequest, response, newEntry);
+        Assertions.assertArrayEquals(requestContent, updatedEntry.getRequestContent());
+
+        final HttpCacheEntry copy = impl.copy(newEntry);
+        Assertions.assertArrayEquals(requestContent, copy.getRequestContent());
+    }
+
     @Test
     void testCreateUpdatedResourceEntry() {
         final Resource resource = HttpTestUtils.makeRandomResource(128);
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
index d279e465f..a9ec8074c 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
@@ -69,6 +69,9 @@ private static boolean equivalent(final HttpCacheEntry expectedValue, final Http
         if (!headersEqual(expectedValue.requestHeaderIterator(), otherValue.requestHeaderIterator())) {
             return false;
         }
+        if (!Arrays.equals(expectedValue.getRequestContent(), otherValue.getRequestContent())) {
+            return false;
+        }
         if (!instantEqual(expectedValue.getRequestInstant(), otherValue.getRequestInstant())) {
             return false;
         }
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java
index 6e8e985d6..124573a40 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java
@@ -49,6 +49,7 @@
 import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
@@ -121,6 +122,26 @@ void testGetCacheEntryFetchesFromCacheOnCacheHitIfNoVariants() throws Exception
         assertSame(entry, result.hit.entry);
     }
 
+    @Test
+    void testStoreRetainsRequestContent() throws Exception {
+        final SimpleHttpRequest request = new SimpleHttpRequest("QUERY", "http://foo.example.com/bar");
+        request.setBody("{\"criteria\":\"value\"}", ContentType.APPLICATION_JSON);
+
+        final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
+        origResponse.setHeader("Date", DateUtils.formatStandardDate(now));
+        origResponse.setHeader("Cache-Control", "max-age=3600, public");
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<CacheHit> hitRef = new AtomicReference<>();
+        impl.store(host, request, origResponse, HttpTestUtils.makeRandomBuffer(128), now, now,
+                HttpTestUtils.countDown(latch, hitRef::set));
+
+        latch.await();
+        final CacheHit hit = hitRef.get();
+        assertNotNull(hit);
+        Assertions.assertArrayEquals(request.getBodyBytes(), hit.entry.getRequestContent());
+    }
+
     @Test
     void testGetCacheEntryReturnsNullIfNoVariantInCache() throws Exception {
         final SimpleHttpRequest origRequest = new SimpleHttpRequest("GET", "http://foo.example.com/bar");
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java
index 429e93ddd..bba9d5150 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java
@@ -48,6 +48,7 @@
 import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
@@ -106,6 +107,19 @@ void testGetCacheEntryFetchesFromCacheOnCacheHitIfNoVariants() {
         assertSame(entry, result.hit.entry);
     }
 
+    @Test
+    void testStoreRetainsRequestContent() {
+        final SimpleHttpRequest request = new SimpleHttpRequest("QUERY", "http://foo.example.com/bar");
+        request.setBody("{\"criteria\":\"value\"}", ContentType.APPLICATION_JSON);
+
+        final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
+        origResponse.setHeader("Date", DateUtils.formatStandardDate(now));
+        origResponse.setHeader("Cache-Control", "max-age=3600, public");
+
+        final CacheHit hit = impl.store(host, request, origResponse, HttpTestUtils.makeRandomBuffer(128), now, now);
+        Assertions.assertArrayEquals(request.getBodyBytes(), hit.entry.getRequestContent());
+    }
+
     @Test
     void testGetCacheEntryReturnsNullIfNoVariantInCache() {
         final SimpleHttpRequest origRequest = new SimpleHttpRequest("GET", "http://foo.example.com/bar");
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java
index 9a70a70ba..54f34b463 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java
@@ -392,6 +392,111 @@ void testSimpleVariantMap() throws Exception {
         HttpCacheEntryMatcher.assertEquivalent(deserialized.getContent(), storageEntry.getContent());
     }
 
+    /**
+     * Serialize and deserialize a cache entry with enclosed request content.
+     */
+    @Test
+    void testSerializeAndDeserializeRequestContent() throws Exception {
+        final String requestContent = "{\"criteria\":\"value\"}";
+        final String content = "Hello World";
+        final ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8);
+        final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
+                "QUERY", "/stuff", HttpTestUtils.headers(new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())),
+                requestContent.getBytes(StandardCharsets.UTF_8),
+                HttpStatus.SC_OK, HttpTestUtils.headers(new BasicHeader(HttpHeaders.CONTENT_TYPE, contentType.toString())),
+                new HeapResource(content.getBytes(contentType.getCharset())),
+                null);
+        final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
+        final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
+
+        final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
+        Assertions.assertEquals(storageEntry.getKey(), deserialized.getKey());
+        HttpCacheEntryMatcher.assertEquivalent(deserialized.getContent(), storageEntry.getContent());
+    }
+
+    /**
+     * Deserialize a cache entry with request content in a fixed format.
+     */
+    @Test
+    void testDeserializeRequestContent() throws Exception {
+        final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
+                "HC-Key: unique-cache-key\n" +
+                "HC-Resource-Length: 11\n" +
+                "HC-Request-Content-Length: 4\n" +
+                "HC-Request-Instant: 1686210849596\n" +
+                "HC-Response-Instant: 1686210849596\n" +
+                "\n" +
+                "QUERY /stuff HTTP/1.1\n" +
+                "Content-Type: text/plain; charset=UTF-8\n" +
+                "\n" +
+                "abcd" +
+                "HTTP/1.1 200 \n" +
+                "Content-Type: text/plain; charset=UTF-8\n" +
+                "\n" +
+                "Hello World";
+        final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
+        final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(bytes1);
+        final HttpCacheEntry cacheEntry = deserialized.getContent();
+        Assertions.assertEquals("QUERY", cacheEntry.getRequestMethod());
+        Assertions.assertArrayEquals("abcd".getBytes(StandardCharsets.UTF_8), cacheEntry.getRequestContent());
+        Assertions.assertArrayEquals("Hello World".getBytes(StandardCharsets.UTF_8), cacheEntry.getResource().get());
+    }
+
+    /**
+     * Deserialize a cache entry with truncated request content.
+     */
+    @Test
+    void testTruncatedRequestContent() {
+        final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
+                "HC-Key: unique-cache-key\n" +
+                "HC-Request-Content-Length: 100\n" +
+                "HC-Request-Instant: 1686210849596\n" +
+                "HC-Response-Instant: 1686210849596\n" +
+                "\n" +
+                "QUERY /stuff HTTP/1.1\n" +
+                "\n" +
+                "abcd";
+        final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
+        final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
+                httpCacheEntrySerializer.deserialize(bytes1));
+        Assertions.assertEquals("Unexpected end of cache content", exception1.getMessage());
+    }
+
+    /**
+     * Deserialize a cache entry with an invalid (negative or exceeding the size
+     * of the serialized representation) request content length.
+     */
+    @Test
+    void testInvalidRequestContentLength() {
+        final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
+                "HC-Key: unique-cache-key\n" +
+                "HC-Request-Content-Length: -2\n" +
+                "HC-Request-Instant: 1686210849596\n" +
+                "HC-Response-Instant: 1686210849596\n" +
+                "\n" +
+                "QUERY /stuff HTTP/1.1\n" +
+                "\n" +
+                "abcd";
+        final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
+        final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
+                httpCacheEntrySerializer.deserialize(bytes1));
+        Assertions.assertEquals("Invalid cache content length: -2", exception1.getMessage());
+
+        final String content2 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
+                "HC-Key: unique-cache-key\n" +
+                "HC-Request-Content-Length: 2000000000\n" +
+                "HC-Request-Instant: 1686210849596\n" +
+                "HC-Response-Instant: 1686210849596\n" +
+                "\n" +
+                "QUERY /stuff HTTP/1.1\n" +
+                "\n" +
+                "abcd";
+        final byte[] bytes2 = content2.getBytes(StandardCharsets.UTF_8);
+        final ResourceIOException exception2 = Assertions.assertThrows(ResourceIOException.class, () ->
+                httpCacheEntrySerializer.deserialize(bytes2));
+        Assertions.assertEquals("Invalid cache content length: 2000000000", exception2.getMessage());
+    }
+
     /**
      * Deserialize cache entries with trailing garbage.
      */