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 5f7f10ee15..e8a7535d9d 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; @@ -94,6 +95,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 +106,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 +117,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 variants) { + this(requestDate, responseDate, method, requestURI, requestHeaders, null, status, responseHeaders, resource, variants); + } + /** * Create a new {@link HttpCacheEntry} with variants. * @param requestDate @@ -176,6 +197,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 +513,14 @@ public Iterator
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. + */ + 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 9b86f0bcc2..e8bd32d6e6 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,27 @@ 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 + */ + 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 +191,7 @@ public HttpCacheEntry create(final Instant requestInstant, request.getMethod(), uri, requestHeaders, + requestContent, response.getCode(), responseHeaders, resource, @@ -213,6 +236,7 @@ public HttpCacheEntry createUpdated( request.getMethod(), uri, requestHeaders, + entry.getRequestContent(), entry.getStatus(), mergedHeaders, entry.getResource(), @@ -233,6 +257,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 658e2754dc..fd2887c51c 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 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 2ab2f16567..3f112f1559 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 be0785e602..42dc952fc0 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 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 8a0f277cca..09673f2dff 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 d279e465f3..a9ec8074cb 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 6e8e985d66..124573a402 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 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 429e93dddb..bba9d51508 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 9a70a70baf..54f34b4637 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. */