### Eclipse Workspace Patch 1.0 #P httpcomponents-client Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (revision 991950) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -31,6 +31,8 @@ import java.net.SocketTimeoutException; import java.util.Date; import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.HeaderElement; @@ -5463,5 +5465,147 @@ } } + /* "The Via general-header field MUST be used by gateways and proxies + * to indicate the intermediate protocols and recipients between the + * user agent and the server on requests, and between the origin server + * and the client on responses." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45 + */ + @Test + public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception { + Capture cap = new Capture(); + request.removeHeaders("Via"); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) + .andReturn(originResponse); + + replayMocks(); + impl.execute(host, request); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + String via = captured.getFirstHeader("Via").getValue(); + assertValidViaHeader(via); + } + + @Test + public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception { + originResponse.removeHeaders("Via"); + backendExpectsAnyRequest().andReturn(originResponse); + replayMocks(); + HttpResponse result = impl.execute(host, request); + verifyMocks(); + assertValidViaHeader(result.getFirstHeader("Via").getValue()); + } + + private void assertValidViaHeader(String via) { + // Via = "Via" ":" 1#( received-protocol received-by [ comment ] ) + // received-protocol = [ protocol-name "/" ] protocol-version + // protocol-name = token + // protocol-version = token + // received-by = ( host [ ":" port ] ) | pseudonym + // pseudonym = token + + String[] parts = via.split("\\s+"); + Assert.assertTrue(parts.length >= 2); + + // received protocol + String receivedProtocol = parts[0]; + String[] protocolParts = receivedProtocol.split("/"); + Assert.assertTrue(protocolParts.length >= 1); + Assert.assertTrue(protocolParts.length <= 2); + + final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+"; + for(String protocolPart : protocolParts) { + Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart)); + } + + // received-by + if (!Pattern.matches(tokenRegexp, parts[1])) { + // host : port + new HttpHost(parts[1]); + } + + // comment + if (parts.length > 2) { + StringBuilder buf = new StringBuilder(parts[2]); + for(int i=3; i cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) + .andReturn(originResponse); + + replayMocks(); + impl.execute(host, request); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + String via = captured.getFirstHeader("Via").getValue(); + String protocol = via.split("\\s+")[0]; + String[] protoParts = protocol.split("/"); + if (protoParts.length > 1) { + Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); + } + Assert.assertEquals("1.0",protoParts[protoParts.length-1]); + } + + @Test + public void testViaHeaderOnResponseProperlyRecordsOriginProtocol() + throws Exception { + + originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content"); + + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + HttpResponse result = impl.execute(host, request); + verifyMocks(); + + String via = result.getFirstHeader("Via").getValue(); + String protocol = via.split("\\s+")[0]; + String[] protoParts = protocol.split("/"); + Assert.assertTrue(protoParts.length >= 1); + Assert.assertTrue(protoParts.length <= 2); + if (protoParts.length > 1) { + Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); + } + Assert.assertEquals("1.0", protoParts[protoParts.length - 1]); + } } \ No newline at end of file Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (revision 991950) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -35,11 +35,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpHost; +import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.ProtocolException; +import org.apache.http.ProtocolVersion; import org.apache.http.RequestLine; import org.apache.http.annotation.ThreadSafe; import org.apache.http.client.ClientProtocolException; @@ -55,6 +57,7 @@ import org.apache.http.message.BasicHttpResponse; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HttpContext; +import org.apache.http.util.VersionInfo; /** * @since 4.1 @@ -358,6 +361,8 @@ // default response context setResponseStatus(context, CacheResponseStatus.CACHE_MISS); + String via = generateViaHeader(request); + if (clientRequestsOurOptions(request)) { setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE); return new OptionsHttp11Response(); @@ -375,6 +380,7 @@ } catch (ProtocolException e) { throw new ClientProtocolException(e); } + request.addHeader("Via",via); responseCache.flushInvalidatedCacheEntriesFor(target, request); @@ -429,6 +435,19 @@ return callBackend(target, request, context); } + private String generateViaHeader(HttpMessage msg) { + final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader()); + final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE; + final ProtocolVersion pv = msg.getProtocolVersion(); + if ("http".equalsIgnoreCase(pv.getProtocol())) { + return String.format("%d.%d localhost (Apache-HttpClient/%s (cache))", + pv.getMajor(), pv.getMinor(), release); + } else { + return String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))", + pv.getProtocol(), pv.getMajor(), pv.getMinor(), release); + } + } + private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) { if (context != null) { context.setAttribute(CACHE_RESPONSE_STATUS, value); @@ -469,6 +488,7 @@ log.debug("Calling the backend"); HttpResponse backendResponse = backend.execute(target, request, context); + backendResponse.addHeader("Via", generateViaHeader(backendResponse)); return handleBackendResponse(target, request, requestDate, getCurrentDate(), backendResponse); Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (revision 991950) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (working copy) @@ -55,6 +55,7 @@ import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; +import org.easymock.Capture; import org.easymock.classextension.EasyMock; import org.junit.Assert; import org.junit.Before; @@ -371,10 +372,11 @@ @Test public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception { mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE); + HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); getCurrentDateReturns(requestDate); - backendCallWasMadeWithRequest(request); + backendCallWasMade(request, resp); getCurrentDateReturns(responseDate); - handleBackendResponseReturnsResponse(request, mockBackendResponse); + handleBackendResponseReturnsResponse(request, resp); replayMocks(); @@ -756,6 +758,30 @@ } @Test + public void testRecordsClientProtocolInViaHeaderIfRequestNotServableFromCache() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0); + req.setHeader("Cache-Control","no-cache"); + HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + Capture cap = new Capture(); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.capture(cap), EasyMock.isA(HttpContext.class))) + .andReturn(resp); + + replayMocks(); + impl.execute(host, req, context); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + String via = captured.getFirstHeader("Via").getValue(); + String proto = via.split("\\s+")[0]; + Assert.assertTrue("http/1.0".equalsIgnoreCase(proto) || + "1.0".equalsIgnoreCase(proto)); + } + + @Test public void testSetsCacheMissContextIfRequestNotServableFromCache() throws Exception { impl = new CachingHttpClient(mockBackend); @@ -773,8 +799,48 @@ Assert.assertEquals(CacheResponseStatus.CACHE_MISS, context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS)); } + + @Test + public void testSetsViaHeaderOnResponseIfRequestNotServableFromCache() + 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), (HttpContext)EasyMock.isNull())) + .andReturn(resp); + + replayMocks(); + HttpResponse result = impl.execute(host, req); + verifyMocks(); + Assert.assertNotNull(result.getFirstHeader("Via")); + } + @Test + public void testSetsViaHeaderOnResponseForCacheMiss() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req1 = 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(); + HttpResponse result = impl.execute(host, req1, new BasicHttpContext()); + verifyMocks(); + Assert.assertNotNull(result.getFirstHeader("Via")); + } + + @Test public void testSetsCacheHitContextIfRequestServedFromCache() throws Exception { impl = new CachingHttpClient(mockBackend); @@ -798,7 +864,31 @@ Assert.assertEquals(CacheResponseStatus.CACHE_HIT, context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS)); } + + @Test + public void testSetsViaHeaderOnResponseIfRequestServedFromCache() + 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), (HttpContext)EasyMock.isNull())) + .andReturn(resp1); + + replayMocks(); + impl.execute(host, req1); + HttpResponse result = impl.execute(host, req2); + verifyMocks(); + Assert.assertNotNull(result.getFirstHeader("Via")); + } + @Test public void testSetsValidatedContextIfRequestWasSuccessfullyValidated() throws Exception { @@ -839,6 +929,45 @@ } @Test + public void testSetsViaHeaderIfRequestWasSuccessfullyValidated() + 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()); + HttpResponse result = impl.execute(host, req2, context); + verifyMocks(); + Assert.assertNotNull(result.getFirstHeader("Via")); + } + + + @Test public void testSetsModuleResponseContextIfValidationRequiredButFailed() throws Exception { Date now = new Date(); @@ -901,7 +1030,39 @@ Assert.assertEquals(CacheResponseStatus.CACHE_HIT, context.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS)); } + + @Test + public void testSetViaHeaderIfValidationFailsButNotRequired() + 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()); + HttpResponse result = impl.execute(host, req2, context); + verifyMocks(); + Assert.assertNotNull(result.getFirstHeader("Via")); + } + + @Test public void testIsSharedCache() { Assert.assertTrue(impl.isSharedCache()); @@ -966,6 +1127,13 @@ EasyMock.same(request), EasyMock.anyObject())).andReturn(mockBackendResponse); } + + private void backendCallWasMade(HttpRequest request, HttpResponse response) throws IOException { + EasyMock.expect(mockBackend.execute( + EasyMock.anyObject(), + EasyMock.same(request), + EasyMock.anyObject())).andReturn(response); + } private void responsePolicyAllowsCaching(boolean allow) { EasyMock.expect(