From 5ba95af1bb8b0b11a3e4705fa03e398c46f9e49f Mon Sep 17 00:00:00 2001 From: Otis Gospodnetic Date: Fri, 23 May 2008 20:55:48 +0000 Subject: [PATCH] SOLR-505 Give RequestHandlers the possiblity to suppress the generation of HTTP caching headers git-svn-id: https://svn.apache.org/repos/asf/lucene/solr/trunk@659657 13f79535-47bb-0310-9956-ffa450edef68 --- .../solr/handler/MoreLikeThisHandler.java | 1 + .../handler/SpellCheckerRequestHandler.java | 1 + .../solr/handler/component/SearchHandler.java | 2 + .../solr/request/SolrQueryResponse.java | 22 +++++- .../apache/solr/servlet/CacheHeaderTest.java | 79 +++++++++++++++++-- .../solr/servlet/CacheHeaderTestBase.java | 14 ++++ .../solr/servlet/SolrDispatchFilter.java | 9 +-- .../servlet/cache/HttpCacheHeaderUtil.java | 56 ++++++++++++- .../org/apache/solr/servlet/cache/Method.java | 21 ++--- 9 files changed, 173 insertions(+), 32 deletions(-) diff --git a/src/java/org/apache/solr/handler/MoreLikeThisHandler.java b/src/java/org/apache/solr/handler/MoreLikeThisHandler.java index 7cd60c82f96..10d1d5cdcf4 100644 --- a/src/java/org/apache/solr/handler/MoreLikeThisHandler.java +++ b/src/java/org/apache/solr/handler/MoreLikeThisHandler.java @@ -79,6 +79,7 @@ public class MoreLikeThisHandler extends RequestHandlerBase public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { RequestHandlerUtils.addExperimentalFormatWarning( rsp ); + rsp.setHttpCaching(true); SolrParams params = req.getParams(); SolrIndexSearcher searcher = req.getSearcher(); diff --git a/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java b/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java index b2b85e9cfcc..8130a62c84e 100644 --- a/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java +++ b/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java @@ -263,6 +263,7 @@ public class SpellCheckerRequestHandler extends RequestHandlerBase implements So @Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { + rsp.setHttpCaching(true); SolrParams p = req.getParams(); String words = p.get("q"); String cmd = p.get("cmd"); diff --git a/src/java/org/apache/solr/handler/component/SearchHandler.java b/src/java/org/apache/solr/handler/component/SearchHandler.java index c1ccd405870..b5613fd63b0 100644 --- a/src/java/org/apache/solr/handler/component/SearchHandler.java +++ b/src/java/org/apache/solr/handler/component/SearchHandler.java @@ -126,6 +126,8 @@ public class SearchHandler extends RequestHandlerBase implements SolrCoreAware final RTimer timer = rb.isDebug() ? new RTimer() : null; + rsp.setHttpCaching(true); + if (timer == null) { // non-debugging prepare phase for( SearchComponent c : components ) { diff --git a/src/java/org/apache/solr/request/SolrQueryResponse.java b/src/java/org/apache/solr/request/SolrQueryResponse.java index 736ffbf5196..fc8637a22ff 100644 --- a/src/java/org/apache/solr/request/SolrQueryResponse.java +++ b/src/java/org/apache/solr/request/SolrQueryResponse.java @@ -72,6 +72,11 @@ public class SolrQueryResponse { // error if this is set... protected Exception err; + /** + * Should this response be tagged with HTTP caching headers? + */ + protected boolean httpCaching=false; + /*** // another way of returning an error int errCode; @@ -199,5 +204,20 @@ public class SolrQueryResponse { public NamedList getToLog() { return toLog; } - + + /** + * Enables or disables the emission of HTTP caching headers for this response. + * @param httpCaching true=emit caching headers, false otherwise + */ + public void setHttpCaching(boolean httpCaching) { + this.httpCaching=httpCaching; + } + + /** + * Should this response emit HTTP caching headers? + * @return true=yes emit headers, false otherwise + */ + public boolean isHttpCaching() { + return this.httpCaching; + } } diff --git a/src/test/org/apache/solr/servlet/CacheHeaderTest.java b/src/test/org/apache/solr/servlet/CacheHeaderTest.java index bc98ca50088..c6ca9757146 100644 --- a/src/test/org/apache/solr/servlet/CacheHeaderTest.java +++ b/src/test/org/apache/solr/servlet/CacheHeaderTest.java @@ -16,18 +16,68 @@ */ package org.apache.solr.servlet; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.util.Date; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.util.DateUtil; /** * A test case for the several HTTP cache headers emitted by Solr */ public class CacheHeaderTest extends CacheHeaderTestBase { - @Override public String getSolrConfigFilename() { return "solrconfig.xml"; } + @Override + public String getSolrConfigFilename() { + return "solrconfig.xml"; + } + + protected static final String FILENAME = "cacheheadertest.csv"; + + protected static final String CHARSET = "UTF-8"; + + protected static final String CONTENTS = "id\n100\n101\n102"; + + public void testCacheVetoHandler() throws Exception { + File f=makeFile(CONTENTS); + HttpMethodBase m=getUpdateMethod("GET"); + m.setQueryString(new NameValuePair[] { new NameValuePair("stream.file",f.getCanonicalPath())}); + getClient().executeMethod(m); + assertEquals(200, m.getStatusCode()); + checkVetoHeaders(m); + } + public void testCacheVetoException() throws Exception { + HttpMethodBase m = getSelectMethod("GET"); + // We force an exception from Solr. This should emit "no-cache" HTTP headers + m.setQueryString(new NameValuePair[] { new NameValuePair("q", "xyz:solr"), + new NameValuePair("qt", "standard") }); + getClient().executeMethod(m); + assertFalse(m.getStatusCode() == 200); + checkVetoHeaders(m); + } + + protected void checkVetoHeaders(HttpMethodBase m) throws Exception { + Header head = m.getResponseHeader("Cache-Control"); + assertNotNull("We got no Cache-Control header", head); + assertEquals("no-cache, no-store", head.getValue()); + + head = m.getResponseHeader("Pragma"); + assertNotNull("We got no Pragma header", head); + assertEquals("no-cache", head.getValue()); + + head = m.getResponseHeader("Expires"); + assertNotNull("We got no Expires header", head); + Date d = DateUtil.parseDate(head.getValue()); + assertTrue("We got no Expires header far in the past", System + .currentTimeMillis() + - d.getTime() > 100000); + } + protected void doLastModified(String method) throws Exception { // We do a first request to get the last modified // This must result in a 200 OK response @@ -162,8 +212,8 @@ public class CacheHeaderTest extends CacheHeaderTestBase { Header head = m.getResponseHeader("Cache-Control"); assertNull("We got a cache-control header in response to POST", head); - - head=m.getResponseHeader("Expires"); + + head = m.getResponseHeader("Expires"); assertNull("We got an Expires header in response to POST", head); } else { HttpMethodBase m = getSelectMethod(method); @@ -172,9 +222,26 @@ public class CacheHeaderTest extends CacheHeaderTestBase { Header head = m.getResponseHeader("Cache-Control"); assertNotNull("We got no cache-control header", head); - - head=m.getResponseHeader("Expires"); - assertNotNull("We got no Expires header in response",head); + + head = m.getResponseHeader("Expires"); + assertNotNull("We got no Expires header in response", head); + } + } + + protected File makeFile(String contents) { + return makeFile(contents, CHARSET); + } + + protected File makeFile(String contents, String charset) { + try { + File f=new File(FILENAME); + Writer out = new OutputStreamWriter(new FileOutputStream(f), + charset); + out.write(contents); + out.close(); + return f; + } catch (Exception e) { + throw new RuntimeException(e); } } } diff --git a/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java b/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java index 61a3df1686b..68cc67a983b 100644 --- a/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java +++ b/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java @@ -93,6 +93,20 @@ public abstract class CacheHeaderTestBase extends SolrExampleTestBase { return m; } + protected HttpMethodBase getUpdateMethod(String method) { + HttpMethodBase m = null; + + if ("GET".equals(method)) { + m=new GetMethod(server.getBaseURL()+"/update/csv"); + } else if ("POST".equals(method)) { + m=new PostMethod(server.getBaseURL()+"/update/csv"); + } else if ("HEAD".equals(method)) { + m=new HeadMethod(server.getBaseURL()+"/update/csv"); + } + + return m; + } + protected HttpClient getClient() { return server.getHttpClient(); } diff --git a/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java b/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java index 664d7f2793d..20aaec28d4c 100644 --- a/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java +++ b/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java @@ -258,13 +258,11 @@ public class SolrDispatchFilter implements Filter } final Method reqMethod = Method.getMethod(req.getMethod()); - if (Method.POST != reqMethod) { - HttpCacheHeaderUtil.setCacheControlHeader(config, resp); - } + HttpCacheHeaderUtil.setCacheControlHeader(config, resp, reqMethod); // unless we have been explicitly told not to, do cache validation // if we fail cache validation, execute the query if (config.getHttpCachingConfig().isNever304() || - !HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, resp)) { + !HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, reqMethod, resp)) { SolrQueryResponse solrRsp = new SolrQueryResponse(); /* even for HEAD requests, we need to execute the handler to * ensure we don't get an error (and to make sure the correct @@ -272,6 +270,7 @@ public class SolrDispatchFilter implements Filter * Content-Type) */ this.execute( req, handler, solrReq, solrRsp ); + HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod); // add info to http headers //TODO: See SOLR-232 and SOLR-267. /*try { @@ -289,7 +288,7 @@ public class SolrDispatchFilter implements Filter // Now write it out QueryResponseWriter responseWriter = core.getQueryResponseWriter(solrReq); response.setContentType(responseWriter.getContentType(solrReq, solrRsp)); - if (Method.HEAD != Method.getMethod(req.getMethod())) { + if (Method.HEAD != reqMethod) { if (responseWriter instanceof BinaryQueryResponseWriter) { BinaryQueryResponseWriter binWriter = (BinaryQueryResponseWriter) responseWriter; binWriter.write(response.getOutputStream(), solrReq, solrRsp); diff --git a/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java b/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java index 26ebf3dc961..4aa8785ccf1 100644 --- a/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java +++ b/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java @@ -35,6 +35,7 @@ import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; import org.apache.solr.request.SolrRequestHandler; import org.apache.commons.codec.binary.Base64; @@ -167,10 +168,16 @@ public final class HttpCacheHeaderUtil { /** * Set the Cache-Control HTTP header (and Expires if needed) * based on the SolrConfig. + * @param conf The config of the SolrCore handling this request + * @param resp The servlet response object to modify + * @param method The request method (GET, POST, ...) used by this request */ public static void setCacheControlHeader(final SolrConfig conf, - final HttpServletResponse resp) { - + final HttpServletResponse resp, final Method method) { + // We do not emit HTTP header for POST and OTHER request types + if (Method.POST==method || Method.OTHER==method) { + return; + } final String cc = conf.getHttpCachingConfig().getCacheControlHeader(); if (null != cc) { resp.setHeader("Cache-Control", cc); @@ -202,10 +209,13 @@ public final class HttpCacheHeaderUtil { */ public static boolean doCacheHeaderValidation(final SolrQueryRequest solrReq, final HttpServletRequest req, + final Method reqMethod, final HttpServletResponse resp) throws IOException { - - final Method reqMethod=Method.getMethod(req.getMethod()); + + if (Method.POST==reqMethod || Method.OTHER==reqMethod) { + return false; + } final long lastMod = HttpCacheHeaderUtil.calcLastModified(solrReq); final String etag = HttpCacheHeaderUtil.calcEtag(solrReq); @@ -295,4 +305,42 @@ public final class HttpCacheHeaderUtil { } return false; } + + /** + * Checks if the downstream request handler wants to avoid HTTP caching of + * the response. + * + * @param solrRsp The Solr response object + * @param resp The HTTP servlet response object + * @param reqMethod The HTTP request type + */ + public static void checkHttpCachingVeto(final SolrQueryResponse solrRsp, + HttpServletResponse resp, final Method reqMethod) { + // For POST we do nothing. They never get cached + if (Method.POST == reqMethod || Method.OTHER == reqMethod) { + return; + } + // If the request handler has not vetoed and there is no + // exception silently return + if (solrRsp.isHttpCaching() && solrRsp.getException() == null) { + return; + } + + // Otherwise we tell the caches that we don't want to cache the response + resp.setHeader("Cache-Control", "no-cache, no-store"); + + // For HTTP/1.0 proxy caches + resp.setHeader("Pragma", "no-cache"); + + // This sets the expiry date to a date in the past + // As long as no time machines get invented this is safe + resp.setHeader("Expires", "Sat, 01 Jan 2000 01:00:00 GMT"); + + // We signal "just modified" just in case some broken + // proxy cache does not follow the above headers + resp.setDateHeader("Last-Modified", System.currentTimeMillis()); + + // We override the ETag with something different + resp.setHeader("ETag", '"'+Long.toHexString(System.currentTimeMillis())+'"'); + } } diff --git a/src/webapp/src/org/apache/solr/servlet/cache/Method.java b/src/webapp/src/org/apache/solr/servlet/cache/Method.java index 6d62d9b9e98..9c793b29c5d 100644 --- a/src/webapp/src/org/apache/solr/servlet/cache/Method.java +++ b/src/webapp/src/org/apache/solr/servlet/cache/Method.java @@ -18,24 +18,13 @@ package org.apache.solr.servlet.cache; public enum Method { - GET("GET"), POST("POST"), HEAD("HEAD"), OTHER(""); - - private final String method; - - Method(String method) { - this.method = method.intern(); - } + GET, POST, HEAD, OTHER; public static Method getMethod(String method) { - method = method.toUpperCase().intern(); - - for (Method m : Method.values()) { - // we can use == because we interned the String objects - if (m.method==method) { - return m; - } + try { + return Method.valueOf(method.toUpperCase()); + } catch (Exception e) { + return OTHER; } - - return OTHER; } }