HTTPCLIENT-982: HTTP cache response status
Contributed by Jonathan Moore <jonathan_moore at comcast.com> git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@990240 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
30eb471da7
commit
02bb1461d8
54
httpclient-cache/src/main/java/org/apache/http/client/cache/CacheResponseStatus.java
vendored
Normal file
54
httpclient-cache/src/main/java/org/apache/http/client/cache/CacheResponseStatus.java
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
* ====================================================================
|
||||||
|
*
|
||||||
|
* This software consists of voluntary contributions made by many
|
||||||
|
* individuals on behalf of the Apache Software Foundation. For more
|
||||||
|
* information on the Apache Software Foundation, please see
|
||||||
|
* <http://www.apache.org/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.apache.http.client.cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This enumeration represents the various ways a response can be generated
|
||||||
|
* by the {@link CachingHttpClient}; if a request is executed with an
|
||||||
|
* {@link org.apache.http.protocol.HttpContext}
|
||||||
|
* then a parameter with one of these values will be registered in the
|
||||||
|
* context.
|
||||||
|
*/
|
||||||
|
public enum CacheResponseStatus {
|
||||||
|
|
||||||
|
/** The response was generated directly by the caching module. */
|
||||||
|
CACHE_MODULE_RESPONSE,
|
||||||
|
|
||||||
|
/** A response was generated from the cache with no requests sent
|
||||||
|
* upstream.
|
||||||
|
*/
|
||||||
|
CACHE_HIT,
|
||||||
|
|
||||||
|
/** The response came from an upstream server. */
|
||||||
|
CACHE_MISS,
|
||||||
|
|
||||||
|
/** The response was generated from the cache after validating the
|
||||||
|
* entry with the origin server.
|
||||||
|
*/
|
||||||
|
VALIDATED;
|
||||||
|
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ import org.apache.http.annotation.ThreadSafe;
|
||||||
import org.apache.http.client.ClientProtocolException;
|
import org.apache.http.client.ClientProtocolException;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.apache.http.client.ResponseHandler;
|
import org.apache.http.client.ResponseHandler;
|
||||||
|
import org.apache.http.client.cache.CacheResponseStatus;
|
||||||
import org.apache.http.client.cache.HeaderConstants;
|
import org.apache.http.client.cache.HeaderConstants;
|
||||||
import org.apache.http.client.cache.HttpCache;
|
import org.apache.http.client.cache.HttpCache;
|
||||||
import org.apache.http.client.cache.HttpCacheEntry;
|
import org.apache.http.client.cache.HttpCacheEntry;
|
||||||
|
@ -61,6 +62,8 @@ import org.apache.http.protocol.HttpContext;
|
||||||
@ThreadSafe // So long as the responseCache implementation is threadsafe
|
@ThreadSafe // So long as the responseCache implementation is threadsafe
|
||||||
public class CachingHttpClient implements HttpClient {
|
public class CachingHttpClient implements HttpClient {
|
||||||
|
|
||||||
|
public static final String CACHE_RESPONSE_STATUS = "http.cache.response.status";
|
||||||
|
|
||||||
private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
|
private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
|
||||||
|
|
||||||
private final AtomicLong cacheHits = new AtomicLong();
|
private final AtomicLong cacheHits = new AtomicLong();
|
||||||
|
@ -352,13 +355,18 @@ public class CachingHttpClient implements HttpClient {
|
||||||
public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
|
public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
|
// default response context
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
|
||||||
|
|
||||||
if (clientRequestsOurOptions(request)) {
|
if (clientRequestsOurOptions(request)) {
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
||||||
return new OptionsHttp11Response();
|
return new OptionsHttp11Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
|
List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
|
||||||
|
|
||||||
for (RequestProtocolError error : fatalError) {
|
for (RequestProtocolError error : fatalError) {
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
||||||
return requestCompliance.getErrorForRequest(error);
|
return requestCompliance.getErrorForRequest(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,6 +401,7 @@ public class CachingHttpClient implements HttpClient {
|
||||||
cacheHits.getAndIncrement();
|
cacheHits.getAndIncrement();
|
||||||
|
|
||||||
if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry)) {
|
if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry)) {
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
|
||||||
return responseGenerator.generateResponse(entry);
|
return responseGenerator.generateResponse(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,8 +413,10 @@ public class CachingHttpClient implements HttpClient {
|
||||||
} catch (IOException ioex) {
|
} catch (IOException ioex) {
|
||||||
if (validityPolicy.mustRevalidate(entry)
|
if (validityPolicy.mustRevalidate(entry)
|
||||||
|| (isSharedCache() && validityPolicy.proxyRevalidate(entry))) {
|
|| (isSharedCache() && validityPolicy.proxyRevalidate(entry))) {
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
||||||
return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
|
return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
|
||||||
} else {
|
} else {
|
||||||
|
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
|
||||||
HttpResponse response = responseGenerator.generateResponse(entry);
|
HttpResponse response = responseGenerator.generateResponse(entry);
|
||||||
response.addHeader(HeaderConstants.WARNING, "111 Revalidation Failed - " + ioex.getMessage());
|
response.addHeader(HeaderConstants.WARNING, "111 Revalidation Failed - " + ioex.getMessage());
|
||||||
log.debug("111 revalidation failed due to exception: " + ioex);
|
log.debug("111 revalidation failed due to exception: " + ioex);
|
||||||
|
@ -418,6 +429,12 @@ public class CachingHttpClient implements HttpClient {
|
||||||
return callBackend(target, request, context);
|
return callBackend(target, request, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
|
||||||
|
if (context != null) {
|
||||||
|
context.setAttribute(CACHE_RESPONSE_STATUS, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean supportsRangeAndContentRangeHeaders() {
|
public boolean supportsRangeAndContentRangeHeaders() {
|
||||||
return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
|
return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
|
||||||
}
|
}
|
||||||
|
@ -472,6 +489,7 @@ public class CachingHttpClient implements HttpClient {
|
||||||
int statusCode = backendResponse.getStatusLine().getStatusCode();
|
int statusCode = backendResponse.getStatusLine().getStatusCode();
|
||||||
if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
|
if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
|
||||||
cacheUpdates.getAndIncrement();
|
cacheUpdates.getAndIncrement();
|
||||||
|
setResponseStatus(context, CacheResponseStatus.VALIDATED);
|
||||||
return responseCache.updateCacheEntry(target, request, cacheEntry,
|
return responseCache.updateCacheEntry(target, request, cacheEntry,
|
||||||
backendResponse, requestDate, responseDate);
|
backendResponse, requestDate, responseDate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,11 +42,15 @@ import org.apache.http.StatusLine;
|
||||||
import org.apache.http.client.ClientProtocolException;
|
import org.apache.http.client.ClientProtocolException;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.apache.http.client.ResponseHandler;
|
import org.apache.http.client.ResponseHandler;
|
||||||
|
import org.apache.http.client.cache.CacheResponseStatus;
|
||||||
import org.apache.http.client.cache.HttpCache;
|
import org.apache.http.client.cache.HttpCache;
|
||||||
import org.apache.http.client.cache.HttpCacheEntry;
|
import org.apache.http.client.cache.HttpCacheEntry;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpUriRequest;
|
import org.apache.http.client.methods.HttpUriRequest;
|
||||||
import org.apache.http.conn.ClientConnectionManager;
|
import org.apache.http.conn.ClientConnectionManager;
|
||||||
|
import org.apache.http.impl.cookie.DateUtils;
|
||||||
import org.apache.http.message.BasicHttpRequest;
|
import org.apache.http.message.BasicHttpRequest;
|
||||||
|
import org.apache.http.message.BasicHttpResponse;
|
||||||
import org.apache.http.params.BasicHttpParams;
|
import org.apache.http.params.BasicHttpParams;
|
||||||
import org.apache.http.params.HttpParams;
|
import org.apache.http.params.HttpParams;
|
||||||
import org.apache.http.protocol.BasicHttpContext;
|
import org.apache.http.protocol.BasicHttpContext;
|
||||||
|
@ -726,6 +730,178 @@ public class TestCachingHttpClient {
|
||||||
Assert.assertTrue(gotException);
|
Assert.assertTrue(gotException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsModuleGeneratedResponseContextForCacheOptionsResponse()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req = new BasicHttpRequest("OPTIONS","*",HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Max-Forwards","0");
|
||||||
|
|
||||||
|
impl.execute(host, req, context);
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsModuleGeneratedResponseContextForFatallyNoncompliantRequest()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req = new HttpGet("http://foo.example.com/");
|
||||||
|
req.setHeader("Range","bytes=0-50");
|
||||||
|
req.setHeader("If-Range","W/\"weak-etag\"");
|
||||||
|
|
||||||
|
impl.execute(host, req, context);
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsCacheMissContextIfRequestNotServableFromCache()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req = new HttpGet("http://foo.example.com/");
|
||||||
|
req.setHeader("Cache-Control","no-cache");
|
||||||
|
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_MISS,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsCacheHitContextIfRequestServedFromCache()
|
||||||
|
throws Exception {
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(new Date()));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=3600");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_HIT,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsValidatedContextIfRequestWasSuccessfullyValidated()
|
||||||
|
throws Exception {
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp2.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp2.setHeader("Content-Length","128");
|
||||||
|
resp2.setHeader("ETag","\"etag\"");
|
||||||
|
resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp2.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp2);
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertEquals(CacheResponseStatus.VALIDATED,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsModuleResponseContextIfValidationRequiredButFailed()
|
||||||
|
throws Exception {
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=5, must-revalidate");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andThrow(new IOException());
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetsModuleResponseContextIfValidationFailsButNotRequired()
|
||||||
|
throws Exception {
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
|
||||||
|
impl = new CachingHttpClient(mockBackend);
|
||||||
|
HttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
HttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length","128");
|
||||||
|
resp1.setHeader("ETag","\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public, max-age=5");
|
||||||
|
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andReturn(resp1);
|
||||||
|
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
|
||||||
|
EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class)))
|
||||||
|
.andThrow(new IOException());
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1, new BasicHttpContext());
|
||||||
|
impl.execute(host, req2, context);
|
||||||
|
verifyMocks();
|
||||||
|
Assert.assertEquals(CacheResponseStatus.CACHE_HIT,
|
||||||
|
context.getAttribute("http.cache.response.context"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsSharedCache() {
|
public void testIsSharedCache() {
|
||||||
Assert.assertTrue(impl.isSharedCache());
|
Assert.assertTrue(impl.isSharedCache());
|
||||||
|
|
Loading…
Reference in New Issue