Repository: pantsbuild/pants

Issue:
Follow redirects for HEAD in RESTfulArtifactCache
Currently we take the requests library default which is to follow redirects for all but HEAD. If a cache server want's to send a HEAD redirect there is no reason we should not follow only for this particular method.


Discussion:

Resolving patch:
diff --git a/src/python/pants/cache/restful_artifact_cache.py b/src/python/pants/cache/restful_artifact_cache.py
--- a/src/python/pants/cache/restful_artifact_cache.py
+++ b/src/python/pants/cache/restful_artifact_cache.py
@@ -108,13 +108,23 @@ def _request(self, method, cache_key, body=None):
       logger.debug('Sending {0} request to {1}'.format(method, url))
       try:
         if 'PUT' == method:
-          response = session.put(url, data=body, timeout=self._write_timeout_secs)
+          response = session.put(url,
+                                 data=body,
+                                 timeout=self._write_timeout_secs,
+                                 allow_redirects=True)
         elif 'GET' == method:
-          response = session.get(url, timeout=self._read_timeout_secs, stream=True)
+          response = session.get(url,
+                                 timeout=self._read_timeout_secs,
+                                 stream=True,
+                                 allow_redirects=True)
         elif 'HEAD' == method:
-          response = session.head(url, timeout=self._read_timeout_secs)
+          response = session.head(url,
+                                  timeout=self._read_timeout_secs,
+                                  allow_redirects=True)
         elif 'DELETE' == method:
-          response = session.delete(url, timeout=self._write_timeout_secs)
+          response = session.delete(url,
+                                    timeout=self._write_timeout_secs,
+                                    allow_redirects=True)
         else:
           raise ValueError('Unknown request method {0}'.format(method))
       except RequestException as e:


Test patch:
diff --git a/tests/python/pants_test/cache/cache_server.py b/tests/python/pants_test/cache/cache_server.py
--- a/tests/python/pants_test/cache/cache_server.py
+++ b/tests/python/pants_test/cache/cache_server.py
@@ -17,6 +17,20 @@
 from pants_test.testutils.file_test_util import exact_files
 
 
+# NB: All API methods should follow redirects, so we always inject one redirect for all handler
+# methods before serving.
+def redirect_once(func):
+  def wrapper(self):
+    if self.path.endswith('/__redir__'):
+      self.path = os.path.dirname(self.path)
+      return func(self)
+    else:
+      self.send_response(307, 'Found')
+      self.send_header('Location', os.path.join(self.path, '__redir__'))
+      self.end_headers()
+  return wrapper
+
+
 # A very trivial server that serves files under the cwd.
 class SimpleRESTHandler(http.server.SimpleHTTPRequestHandler):
   def __init__(self, request, client_address, server):
@@ -24,9 +38,15 @@ def __init__(self, request, client_address, server):
     # Old-style class, so we must invoke __init__ this way.
     http.server.SimpleHTTPRequestHandler.__init__(self, request, client_address, server)
 
+  @redirect_once
   def do_HEAD(self):
     return http.server.SimpleHTTPRequestHandler.do_HEAD(self)
 
+  @redirect_once
+  def do_GET(self):
+    return http.server.SimpleHTTPRequestHandler.do_GET(self)
+
+  @redirect_once
   def do_PUT(self):
     path = self.translate_path(self.path)
     content_length = int(self.headers.get('content-length'))
@@ -37,6 +57,7 @@ def do_PUT(self):
     self.send_response(200)
     self.end_headers()
 
+  @redirect_once
   def do_DELETE(self):
     path = self.translate_path(self.path)
     if os.path.exists(path):
@@ -58,15 +79,19 @@ def _return_failed(self):
     self.send_response(401, 'Forced test failure')
     self.end_headers()
 
+  @redirect_once
   def do_HEAD(self):
     return self._return_failed()
 
+  @redirect_once
   def do_GET(self):
     return self._return_failed()
 
+  @redirect_once
   def do_PUT(self):
     return self._return_failed()
 
+  @redirect_once
   def do_DELETE(self):
     return self._return_failed()