Fetcher tries all proxies when a secondary chunk download error occurs.

This is a fix to issue 18143:

* New test cases for asserting the desired behavior: if a transfer of
  a secondary chunk within a multi-chunk fetch fails, then the fetcher
  needs to retry with other available proxies; it will only fail when no
  additional proxies are available.  The tests ensure both success (one
  of the proxies eventually succeeds) and failure (all proxies fail)
  cases.

* Small fix to LibcurlHttpFetcher to retry with other proxies upon
  failure (error value) of a secondary chunk.

Other changes applied in the course of this fix:

* Massive refactoring of http_fetcher_unittest: substituted template
  specialization in typed test setup with proper subclassing, resulting
  in a safer and more maintainable infrastructure;  extended URLs to
  include all (most) parameters pertaining to test workload, such as
  download size, flakiness, etc.

* Respective changes to test_http_server: it is now much more
  independent of particular kind of tests, and more easily
  parametrizable.  Also, generalized several internal methods for better
  readability and extensibility, such as writing of arbitrary payloads,
  parsing headers,

* Migrated common definitions into http_common.{h,cc} (universal
  HTTP-related stuff) and http_fetcher_unittest.h (shared definitions
  pertaining to unit tests).

* Extended direct proxy resolver to generate a list of (non-) proxies,
  so we can unit test proxy failure.  Also, better logging to improve
  testability.

* Some renaming of classes for better consistency.

BUG=chromium-os:18143
TEST=unit tests

Change-Id: Ib90b53394d7e47184d9953df8fc80348921e8af0
Reviewed-on: https://gerrit.chromium.org/gerrit/12092
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/SConstruct b/SConstruct
index 22d3d45..d73c4e8 100644
--- a/SConstruct
+++ b/SConstruct
@@ -256,6 +256,7 @@
                    flimflam_proxy.cc
                    full_update_generator.cc
                    graph_utils.cc
+                   http_common.cc
                    http_fetcher.cc
                    libcurl_http_fetcher.cc
                    marshal.glibmarshal.c
diff --git a/http_common.cc b/http_common.cc
new file mode 100644
index 0000000..647faba
--- /dev/null
+++ b/http_common.cc
@@ -0,0 +1,53 @@
+// Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Implementation of common HTTP related functions.
+
+#include "base/basictypes.h"
+
+#include "update_engine/http_common.h"
+
+
+const char *GetHttpResponseDescription(HttpResponseCode code) {
+  static const struct {
+    HttpResponseCode code;
+    const char* description;
+  } http_response_table[] = {
+    { kHttpResponseOk,                  "OK" },
+    { kHttpResponseCreated,             "Created" },
+    { kHttpResponseAccepted,            "Accepted" },
+    { kHttpResponseNonAuthInfo,         "Non-Authoritative Information" },
+    { kHttpResponseNoContent,           "No Content" },
+    { kHttpResponseResetContent,        "Reset Content" },
+    { kHttpResponsePartialContent,      "Partial Content" },
+    { kHttpResponseMultipleChoices,     "Multiple Choices" },
+    { kHttpResponseMovedPermanently,    "Moved Permanently" },
+    { kHttpResponseFound,               "Found" },
+    { kHttpResponseSeeOther,            "See Other" },
+    { kHttpResponseNotModified,         "Not Modified" },
+    { kHttpResponseUseProxy,            "Use Proxy" },
+    { kHttpResponseTempRedirect,        "Temporary Redirect" },
+    { kHttpResponseBadRequest,          "Bad Request" },
+    { kHttpResponseUnauth,              "Unauthorized" },
+    { kHttpResponseForbidden,           "Forbidden" },
+    { kHttpResponseNotFound,            "Not Found" },
+    { kHttpResponseRequestTimeout,      "Request Timeout" },
+    { kHttpResponseInternalServerError, "Internal Server Error" },
+    { kHttpResponseNotImplemented,      "Not Implemented" },
+    { kHttpResponseServiceUnavailable,  "Service Unavailable" },
+    { kHttpResponseVersionNotSupported, "HTTP Version Not Supported" },
+  };
+
+  bool is_found = false;
+  size_t i;
+  for (i = 0; i < ARRAYSIZE_UNSAFE(http_response_table); i++)
+    if ((is_found = (http_response_table[i].code == code)))
+      break;
+
+  return (is_found ? http_response_table[i].description : "(unsupported)");
+}
+
+HttpResponseCode StringToHttpResponseCode(const char *s) {
+  return static_cast<HttpResponseCode>(strtoul(s, NULL, 10));
+}
diff --git a/http_common.h b/http_common.h
new file mode 100644
index 0000000..ecd90e1
--- /dev/null
+++ b/http_common.h
@@ -0,0 +1,47 @@
+// Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file contains general definitions used in implementing, testing and
+// emulating communication over HTTP.
+
+#ifndef CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_COMMON_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_COMMON_H__
+
+#include <stdlib.h>
+
+// Enumeration type for HTTP response codes.
+enum HttpResponseCode {
+  kHttpResponseUndefined           = 0,
+  kHttpResponseOk                  = 200,
+  kHttpResponseCreated             = 201,
+  kHttpResponseAccepted            = 202,
+  kHttpResponseNonAuthInfo         = 203,
+  kHttpResponseNoContent           = 204,
+  kHttpResponseResetContent        = 205,
+  kHttpResponsePartialContent      = 206,
+  kHttpResponseMultipleChoices     = 300,
+  kHttpResponseMovedPermanently    = 301,
+  kHttpResponseFound               = 302,
+  kHttpResponseSeeOther            = 303,
+  kHttpResponseNotModified         = 304,
+  kHttpResponseUseProxy            = 305,
+  kHttpResponseTempRedirect        = 307,
+  kHttpResponseBadRequest          = 400,
+  kHttpResponseUnauth              = 401,
+  kHttpResponseForbidden           = 403,
+  kHttpResponseNotFound            = 404,
+  kHttpResponseRequestTimeout      = 408,
+  kHttpResponseInternalServerError = 500,
+  kHttpResponseNotImplemented      = 501,
+  kHttpResponseServiceUnavailable  = 503,
+  kHttpResponseVersionNotSupported = 505,
+};
+
+// Returns a standard HTTP status line string for a given response code.
+const char *GetHttpResponseDescription(HttpResponseCode code);
+
+// Converts a string beginning with an HTTP error code into numerical value.
+HttpResponseCode StringToHttpResponseCode(const char *s);
+
+#endif  // CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_COMMON_H__
diff --git a/http_fetcher_unittest.cc b/http_fetcher_unittest.cc
index 2aabfd9..1ffba72 100644
--- a/http_fetcher_unittest.cc
+++ b/http_fetcher_unittest.cc
@@ -14,6 +14,8 @@
 #include <glib.h>
 #include <gtest/gtest.h>
 
+#include "update_engine/http_common.h"
+#include "update_engine/http_fetcher_unittest.h"
 #include "update_engine/libcurl_http_fetcher.h"
 #include "update_engine/mock_http_fetcher.h"
 #include "update_engine/multi_range_http_fetcher.h"
@@ -24,71 +26,49 @@
 using std::string;
 using std::vector;
 
+namespace {
+
+const int kBigLength           = 100000;
+const int kMediumLength        = 1000;
+const int kFlakyTruncateLength = 9000;
+const int kFlakySleepEvery     = 7;
+const int kFlakySleepSecs      = 10;
+
+}  // namespace
+
 namespace chromeos_update_engine {
 
-namespace {
-// WARNING, if you update these, you must also update test_http_server.cc.
-const char* const kServerPort = "8088";
-const int kBigSize = 100000;
-string LocalServerUrlForPath(const string& path) {
-  return string("http://127.0.0.1:") + kServerPort + path;
-}
+static const char *kUnusedUrl = "unused://unused";
+
+static inline string LocalServerUrlForPath(const string& path) {
+  return (string("http://127.0.0.1:") + base::StringPrintf("%d", kServerPort) + path);
 }
 
-template <typename T>
-class HttpFetcherTest : public ::testing::Test {
- public:
-  HttpFetcher* NewLargeFetcher() = 0;
-  HttpFetcher* NewSmallFetcher() = 0;
-  string BigUrl() const = 0;
-  string SmallUrl() const = 0;
-  string ErrorUrl() const = 0;
-  bool IsMock() const = 0;
-  bool IsMulti() const = 0;
-};
 
-class NullHttpServer {
+//
+// Class hierarchy for HTTP server implementations.
+//
+
+class HttpServer {
  public:
-  NullHttpServer() : started_(true) {}
-  ~NullHttpServer() {}
+  // This makes it an abstract class (dirty but works).
+  virtual ~HttpServer() = 0;
+
   bool started_;
 };
 
+HttpServer::~HttpServer() {}
 
-template <>
-class HttpFetcherTest<MockHttpFetcher> : public ::testing::Test {
+
+class NullHttpServer : public HttpServer {
  public:
-  HttpFetcher* NewLargeFetcher() {
-    vector<char> big_data(1000000);
-    return new MockHttpFetcher(
-        big_data.data(),
-        big_data.size(),
-        reinterpret_cast<ProxyResolver*>(&proxy_resolver_));
+  NullHttpServer() {
+    started_ = true;
   }
-  HttpFetcher* NewSmallFetcher() {
-    return new MockHttpFetcher(
-        "x",
-        1,
-        reinterpret_cast<ProxyResolver*>(&proxy_resolver_));
-  }
-  string BigUrl() const {
-    return "unused://unused";
-  }
-  string SmallUrl() const {
-    return "unused://unused";
-  }
-  string ErrorUrl() const {
-    return "unused://unused";
-  }
-  bool IsMock() const { return true; }
-  bool IsMulti() const { return false; }
-  typedef NullHttpServer HttpServer;
-  void IgnoreServerAborting(HttpServer* server) const {}
-  
-  DirectProxyResolver proxy_resolver_;
 };
 
-class PythonHttpServer {
+
+class PythonHttpServer : public HttpServer {
  public:
   PythonHttpServer() {
     char *argv[2] = {strdup("./test_http_server"), NULL};
@@ -129,8 +109,8 @@
     }
     free(argv[0]);
     LOG(INFO) << "gdb attach now!";
-    return;
   }
+
   ~PythonHttpServer() {
     if (!started_)
       return;
@@ -143,15 +123,84 @@
       EXPECT_EQ(0, rc);
     waitpid(pid_, NULL, 0);
   }
+
   GPid pid_;
-  bool started_;
   bool validate_quit_;
 };
 
-template <>
-class HttpFetcherTest<LibcurlHttpFetcher> : public ::testing::Test {
+
+
+//
+// Class hierarchy for HTTP fetcher test wrappers.
+//
+
+class AnyHttpFetcherTest {
  public:
-  virtual HttpFetcher* NewLargeFetcher() {
+  virtual HttpFetcher* NewLargeFetcher(size_t num_proxies) = 0;
+  HttpFetcher* NewLargeFetcher() {
+    return NewLargeFetcher(1);
+  }
+
+  virtual HttpFetcher* NewSmallFetcher(size_t num_proxies) = 0;
+  HttpFetcher* NewSmallFetcher() {
+    return NewSmallFetcher(1);
+  }
+
+  virtual string BigUrl() const { return kUnusedUrl; }
+  virtual string SmallUrl() const { return kUnusedUrl; }
+  virtual string ErrorUrl() const { return kUnusedUrl; }
+
+  virtual bool IsMock() const = 0;
+  virtual bool IsMulti() const = 0;
+
+  virtual void IgnoreServerAborting(HttpServer* server) const {}
+
+  virtual HttpServer *CreateServer() = 0;
+
+ protected:
+  DirectProxyResolver proxy_resolver_;
+};
+
+class MockHttpFetcherTest : public AnyHttpFetcherTest {
+ public:
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewLargeFetcher;
+  virtual HttpFetcher* NewLargeFetcher(size_t num_proxies) {
+    vector<char> big_data(1000000);
+    CHECK(num_proxies > 0);
+    proxy_resolver_.set_num_proxies(num_proxies);
+    return new MockHttpFetcher(
+        big_data.data(),
+        big_data.size(),
+        reinterpret_cast<ProxyResolver*>(&proxy_resolver_));
+  }
+
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewSmallFetcher;
+  virtual HttpFetcher* NewSmallFetcher(size_t num_proxies) {
+    CHECK(num_proxies > 0);
+    proxy_resolver_.set_num_proxies(num_proxies);
+    return new MockHttpFetcher(
+        "x",
+        1,
+        reinterpret_cast<ProxyResolver*>(&proxy_resolver_));
+  }
+
+  virtual bool IsMock() const { return true; }
+  virtual bool IsMulti() const { return false; }
+
+  virtual HttpServer *CreateServer() {
+    return new NullHttpServer;
+  }
+};
+
+class LibcurlHttpFetcherTest : public AnyHttpFetcherTest {
+ public:
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewLargeFetcher;
+  virtual HttpFetcher* NewLargeFetcher(size_t num_proxies) {
+    CHECK(num_proxies > 0);
+    proxy_resolver_.set_num_proxies(num_proxies);
     LibcurlHttpFetcher *ret = new
         LibcurlHttpFetcher(reinterpret_cast<ProxyResolver*>(&proxy_resolver_));
     // Speed up test execution.
@@ -161,37 +210,48 @@
     ret->SetBuildType(false);
     return ret;
   }
-  HttpFetcher* NewSmallFetcher() {
-    return NewLargeFetcher();
+
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewSmallFetcher;
+  virtual HttpFetcher* NewSmallFetcher(size_t num_proxies) {
+    return NewLargeFetcher(num_proxies);
   }
-  string BigUrl() const {
-    return LocalServerUrlForPath("/big");
+
+  virtual string BigUrl() const {
+    return LocalServerUrlForPath(base::StringPrintf("/download/%d",
+                                                    kBigLength));
   }
-  string SmallUrl() const {
+  virtual string SmallUrl() const {
     return LocalServerUrlForPath("/foo");
   }
-  string ErrorUrl() const {
+  virtual string ErrorUrl() const {
     return LocalServerUrlForPath("/error");
   }
-  bool IsMock() const { return false; }
-  bool IsMulti() const { return false; }
-  typedef PythonHttpServer HttpServer;
-  void IgnoreServerAborting(HttpServer* server) const {
+
+  virtual bool IsMock() const { return false; }
+  virtual bool IsMulti() const { return false; }
+
+  virtual void IgnoreServerAborting(HttpServer* server) const {
     PythonHttpServer *pyserver = reinterpret_cast<PythonHttpServer*>(server);
     pyserver->validate_quit_ = false;
   }
-  DirectProxyResolver proxy_resolver_;
+
+  virtual HttpServer *CreateServer() {
+    return new PythonHttpServer;
+  }
 };
 
-template <>
-class HttpFetcherTest<MultiRangeHTTPFetcher>
-    : public HttpFetcherTest<LibcurlHttpFetcher> {
+class MultiRangeHttpFetcherTest : public LibcurlHttpFetcherTest {
  public:
-  HttpFetcher* NewLargeFetcher() {
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewLargeFetcher;
+  virtual HttpFetcher* NewLargeFetcher(size_t num_proxies) {
+    CHECK(num_proxies > 0);
+    proxy_resolver_.set_num_proxies(num_proxies);
     ProxyResolver* resolver =
         reinterpret_cast<ProxyResolver*>(&proxy_resolver_);
-    MultiRangeHTTPFetcher *ret =
-        new MultiRangeHTTPFetcher(new LibcurlHttpFetcher(resolver));
+    MultiRangeHttpFetcher *ret =
+        new MultiRangeHttpFetcher(new LibcurlHttpFetcher(resolver));
     ret->ClearRanges();
     ret->AddRange(0, -1);
     // Speed up test execution.
@@ -201,20 +261,47 @@
     ret->SetBuildType(false);
     return ret;
   }
-  bool IsMulti() const { return true; }
-  DirectProxyResolver proxy_resolver_;
+
+  // Necessary to unhide the definition in the base class.
+  using AnyHttpFetcherTest::NewSmallFetcher;
+  virtual HttpFetcher* NewSmallFetcher(size_t num_proxies) {
+    return NewLargeFetcher(num_proxies);
+  }
+
+  virtual bool IsMulti() const { return true; }
 };
 
-typedef ::testing::Types<LibcurlHttpFetcher,
-                         MockHttpFetcher,
-                         MultiRangeHTTPFetcher>
-HttpFetcherTestTypes;
+
+//
+// Infrastructure for type tests of HTTP fetcher.
+// See: http://code.google.com/p/googletest/wiki/AdvancedGuide#Typed_Tests
+//
+
+// Fixture class template. We use an explicit constraint to guarantee that it
+// can only be instantiated with an AnyHttpFetcherTest type, see:
+// http://www2.research.att.com/~bs/bs_faq2.html#constraints
+template <typename T>
+class HttpFetcherTest : public ::testing::Test {
+ public:
+  T test_;
+
+ private:
+  static void TypeConstraint(T *a) {
+    AnyHttpFetcherTest *b = a;
+  }
+};
+
+// Test case types list.
+typedef ::testing::Types<LibcurlHttpFetcherTest,
+                         MockHttpFetcherTest,
+                         MultiRangeHttpFetcherTest> HttpFetcherTestTypes;
 TYPED_TEST_CASE(HttpFetcherTest, HttpFetcherTestTypes);
 
+
 namespace {
 class HttpFetcherTestDelegate : public HttpFetcherDelegate {
  public:
-  HttpFetcherTestDelegate(void) :
+  HttpFetcherTestDelegate() :
       is_expect_error_(false), times_transfer_complete_called_(0),
       times_transfer_terminated_called_(0), times_received_bytes_called_(0) {}
 
@@ -230,9 +317,9 @@
 
   virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
     if (is_expect_error_)
-      EXPECT_EQ(404, fetcher->http_response_code());
+      EXPECT_EQ(kHttpResponseNotFound, fetcher->http_response_code());
     else
-      EXPECT_EQ(200, fetcher->http_response_code());
+      EXPECT_EQ(kHttpResponseOk, fetcher->http_response_code());
     g_main_loop_quit(loop_);
 
     // Update counter
@@ -272,13 +359,13 @@
   {
     HttpFetcherTestDelegate delegate;
     delegate.loop_ = loop;
-    scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewSmallFetcher());
     fetcher->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
-    StartTransferArgs start_xfer_args = {fetcher.get(), this->SmallUrl()};
+    StartTransferArgs start_xfer_args = {fetcher.get(), this->test_.SmallUrl()};
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
     g_main_loop_run(loop);
@@ -291,13 +378,13 @@
   {
     HttpFetcherTestDelegate delegate;
     delegate.loop_ = loop;
-    scoped_ptr<HttpFetcher> fetcher(this->NewLargeFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewLargeFetcher());
     fetcher->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
-    StartTransferArgs start_xfer_args = {fetcher.get(), this->BigUrl()};
+    StartTransferArgs start_xfer_args = {fetcher.get(), this->test_.BigUrl()};
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
     g_main_loop_run(loop);
@@ -308,7 +395,7 @@
 // Issue #9648: when server returns an error HTTP response, the fetcher needs to
 // terminate transfer prematurely, rather than try to process the error payload.
 TYPED_TEST(HttpFetcherTest, ErrorTest) {
-  if (this->IsMock() || this->IsMulti())
+  if (this->test_.IsMock() || this->test_.IsMulti())
     return;
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
@@ -318,13 +405,16 @@
     // Delegate should expect an error response.
     delegate.is_expect_error_ = true;
 
-    scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewSmallFetcher());
     fetcher->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
-    StartTransferArgs start_xfer_args = {fetcher.get(), this->ErrorUrl()};
+    StartTransferArgs start_xfer_args = {
+      fetcher.get(),
+      this->test_.ErrorUrl()
+    };
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
     g_main_loop_run(loop);
@@ -341,7 +431,6 @@
   g_main_loop_unref(loop);
 }
 
-
 namespace {
 class PausingHttpFetcherTestDelegate : public HttpFetcherDelegate {
  public:
@@ -383,17 +472,18 @@
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     PausingHttpFetcherTestDelegate delegate;
-    scoped_ptr<HttpFetcher> fetcher(this->NewLargeFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewLargeFetcher());
     delegate.paused_ = false;
     delegate.loop_ = loop;
     delegate.fetcher_ = fetcher.get();
     fetcher->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
-    guint callback_id = g_timeout_add(500, UnpausingTimeoutCallback, &delegate);
-    fetcher->BeginTransfer(this->BigUrl());
+    guint callback_id = g_timeout_add(kHttpResponseInternalServerError,
+                                      UnpausingTimeoutCallback, &delegate);
+    fetcher->BeginTransfer(this->test_.BigUrl());
 
     g_main_loop_run(loop);
     g_source_remove(callback_id);
@@ -449,21 +539,22 @@
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     AbortingHttpFetcherTestDelegate delegate;
-    delegate.fetcher_.reset(this->NewLargeFetcher());
+    delegate.fetcher_.reset(this->test_.NewLargeFetcher());
     delegate.once_ = true;
     delegate.callback_once_ = true;
     delegate.loop_ = loop;
     delegate.fetcher_->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    this->IgnoreServerAborting(&server);
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    this->test_.IgnoreServerAborting(server.get());
+    ASSERT_TRUE(server->started_);
+
     GSource* timeout_source_;
     timeout_source_ = g_timeout_source_new(0);  // ms
     g_source_set_callback(timeout_source_, AbortingTimeoutCallback, &delegate,
                           NULL);
     g_source_attach(timeout_source_, NULL);
-    delegate.fetcher_->BeginTransfer(this->BigUrl());
+    delegate.fetcher_->BeginTransfer(this->test_.BigUrl());
 
     g_main_loop_run(loop);
     CHECK(!delegate.once_);
@@ -482,7 +573,7 @@
   }
   virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
     EXPECT_TRUE(successful);
-    EXPECT_EQ(206, fetcher->http_response_code());
+    EXPECT_EQ(kHttpResponsePartialContent, fetcher->http_response_code());
     g_main_loop_quit(loop_);
   }
   virtual void TransferTerminated(HttpFetcher* fetcher) {
@@ -494,29 +585,32 @@
 }  // namespace {}
 
 TYPED_TEST(HttpFetcherTest, FlakyTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     FlakyHttpFetcherTestDelegate delegate;
     delegate.loop_ = loop;
-    scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewSmallFetcher());
     fetcher->set_delegate(&delegate);
 
-    typename TestFixture::HttpServer server;
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
     StartTransferArgs start_xfer_args = {
       fetcher.get(),
-      LocalServerUrlForPath("/flaky")
+      LocalServerUrlForPath(StringPrintf("/flaky/%d/%d/%d/%d", kBigLength,
+                                         kFlakyTruncateLength,
+                                         kFlakySleepEvery,
+                                         kFlakySleepSecs))
     };
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
     g_main_loop_run(loop);
 
     // verify the data we get back
-    ASSERT_EQ(100000, delegate.data.size());
-    for (int i = 0; i < 100000; i += 10) {
+    ASSERT_EQ(kBigLength, delegate.data.size());
+    for (int i = 0; i < kBigLength; i += 10) {
       // Assert so that we don't flood the screen w/ EXPECT errors on failure.
       ASSERT_EQ(delegate.data.substr(i, 10), "abcdefghij");
     }
@@ -552,18 +646,18 @@
 
 
 TYPED_TEST(HttpFetcherTest, FailureTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     FailureHttpFetcherTestDelegate delegate;
     delegate.loop_ = loop;
-    scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewSmallFetcher());
     fetcher->set_delegate(&delegate);
 
     StartTransferArgs start_xfer_args = {
       fetcher.get(),
-      LocalServerUrlForPath(this->SmallUrl())
+      LocalServerUrlForPath(this->test_.SmallUrl())
     };
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
@@ -575,19 +669,22 @@
 }
 
 TYPED_TEST(HttpFetcherTest, ServerDiesTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     FailureHttpFetcherTestDelegate delegate;
     delegate.loop_ = loop;
     delegate.server_ = new PythonHttpServer;
-    scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewSmallFetcher());
     fetcher->set_delegate(&delegate);
 
     StartTransferArgs start_xfer_args = {
       fetcher.get(),
-      LocalServerUrlForPath("/flaky")
+      LocalServerUrlForPath(StringPrintf("/flaky/%d/%d/%d/%d", kBigLength,
+                                         kFlakyTruncateLength,
+                                         kFlakySleepEvery,
+                                         kFlakySleepSecs))
     };
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
@@ -599,7 +696,10 @@
 }
 
 namespace {
-const int kRedirectCodes[] = { 301, 302, 303, 307 };
+const HttpResponseCode kRedirectCodes[] = {
+  kHttpResponseMovedPermanently, kHttpResponseFound, kHttpResponseSeeOther,
+  kHttpResponseTempRedirect
+};
 
 class RedirectHttpFetcherTestDelegate : public HttpFetcherDelegate {
  public:
@@ -612,10 +712,10 @@
   virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
     EXPECT_EQ(expected_successful_, successful);
     if (expected_successful_)
-      EXPECT_EQ(200, fetcher->http_response_code());
+      EXPECT_EQ(kHttpResponseOk, fetcher->http_response_code());
     else {
-      EXPECT_GE(fetcher->http_response_code(), 301);
-      EXPECT_LE(fetcher->http_response_code(), 307);
+      EXPECT_GE(fetcher->http_response_code(), kHttpResponseMovedPermanently);
+      EXPECT_LE(fetcher->http_response_code(), kHttpResponseTempRedirect);
     }
     g_main_loop_quit(loop_);
   }
@@ -644,8 +744,8 @@
   g_main_loop_run(loop);
   if (expected_successful) {
     // verify the data we get back
-    ASSERT_EQ(1000, delegate.data.size());
-    for (int i = 0; i < 1000; i += 10) {
+    ASSERT_EQ(kMediumLength, delegate.data.size());
+    for (int i = 0; i < kMediumLength; i += 10) {
       // Assert so that we don't flood the screen w/ EXPECT errors on failure.
       ASSERT_EQ(delegate.data.substr(i, 10), "abcdefghij");
     }
@@ -655,43 +755,50 @@
 }  // namespace {}
 
 TYPED_TEST(HttpFetcherTest, SimpleRedirectTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
+
   for (size_t c = 0; c < arraysize(kRedirectCodes); ++c) {
-    const string url = base::StringPrintf("/redirect/%d/medium",
-                                          kRedirectCodes[c]);
-    RedirectTest(true, url, this->NewLargeFetcher());
+    const string url = base::StringPrintf("/redirect/%d/download/%d",
+                                          kRedirectCodes[c],
+                                          kMediumLength);
+    RedirectTest(true, url, this->test_.NewLargeFetcher());
   }
 }
 
 TYPED_TEST(HttpFetcherTest, MaxRedirectTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
+
   string url;
   for (int r = 0; r < LibcurlHttpFetcher::kMaxRedirects; r++) {
     url += base::StringPrintf("/redirect/%d",
                               kRedirectCodes[r % arraysize(kRedirectCodes)]);
   }
-  url += "/medium";
-  RedirectTest(true, url, this->NewLargeFetcher());
+  url += base::StringPrintf("/download/%d", kMediumLength);
+  RedirectTest(true, url, this->test_.NewLargeFetcher());
 }
 
 TYPED_TEST(HttpFetcherTest, BeyondMaxRedirectTest) {
-  if (this->IsMock())
+  if (this->test_.IsMock())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
+
   string url;
   for (int r = 0; r < LibcurlHttpFetcher::kMaxRedirects + 1; r++) {
     url += base::StringPrintf("/redirect/%d",
                               kRedirectCodes[r % arraysize(kRedirectCodes)]);
   }
-  url += "/medium";
-  RedirectTest(false, url, this->NewLargeFetcher());
+  url += base::StringPrintf("/download/%d", kMediumLength);
+  RedirectTest(false, url, this->test_.NewLargeFetcher());
 }
 
 namespace {
@@ -706,7 +813,7 @@
   }
   virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
     EXPECT_EQ(fetcher, fetcher_.get());
-    EXPECT_EQ(expected_response_code_ != 0, successful);
+    EXPECT_EQ(expected_response_code_ != kHttpResponseUndefined, successful);
     if (expected_response_code_ != 0)
       EXPECT_EQ(expected_response_code_, fetcher->http_response_code());
     // Destroy the fetcher (because we're allowed to).
@@ -727,14 +834,14 @@
                const vector<pair<off_t, off_t> >& ranges,
                const string& expected_prefix,
                off_t expected_size,
-               int expected_response_code) {
+               HttpResponseCode expected_response_code) {
   GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
   {
     MultiHttpFetcherTestDelegate delegate(expected_response_code);
     delegate.loop_ = loop;
     delegate.fetcher_.reset(fetcher_in);
-    MultiRangeHTTPFetcher* multi_fetcher =
-        dynamic_cast<MultiRangeHTTPFetcher*>(fetcher_in);
+    MultiRangeHttpFetcher* multi_fetcher =
+        dynamic_cast<MultiRangeHttpFetcher*>(fetcher_in);
     ASSERT_TRUE(multi_fetcher);
     multi_fetcher->ClearRanges();
     for (vector<pair<off_t, off_t> >::const_iterator it = ranges.begin(),
@@ -760,75 +867,126 @@
 }  // namespace {}
 
 TYPED_TEST(HttpFetcherTest, MultiHttpFetcherSimpleTest) {
-  if (!this->IsMulti())
+  if (!this->test_.IsMulti())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
 
   vector<pair<off_t, off_t> > ranges;
   ranges.push_back(make_pair(0, 25));
   ranges.push_back(make_pair(99, -1));
-  MultiTest(this->NewLargeFetcher(),
-            this->BigUrl(),
+  MultiTest(this->test_.NewLargeFetcher(),
+            this->test_.BigUrl(),
             ranges,
             "abcdefghijabcdefghijabcdejabcdefghijabcdef",
-            kBigSize - (99 - 25),
-            206);
+            kBigLength - (99 - 25),
+            kHttpResponsePartialContent);
 }
 
 TYPED_TEST(HttpFetcherTest, MultiHttpFetcherLengthLimitTest) {
-  if (!this->IsMulti())
+  if (!this->test_.IsMulti())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
 
   vector<pair<off_t, off_t> > ranges;
   ranges.push_back(make_pair(0, 24));
-  MultiTest(this->NewLargeFetcher(),
-            this->BigUrl(),
+  MultiTest(this->test_.NewLargeFetcher(),
+            this->test_.BigUrl(),
             ranges,
             "abcdefghijabcdefghijabcd",
             24,
-            200);
+            kHttpResponseOk);
 }
 
 TYPED_TEST(HttpFetcherTest, MultiHttpFetcherMultiEndTest) {
-  if (!this->IsMulti())
+  if (!this->test_.IsMulti())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
 
   vector<pair<off_t, off_t> > ranges;
-  ranges.push_back(make_pair(kBigSize - 2, -1));
-  ranges.push_back(make_pair(kBigSize - 3, -1));
-  MultiTest(this->NewLargeFetcher(),
-            this->BigUrl(),
+  ranges.push_back(make_pair(kBigLength - 2, -1));
+  ranges.push_back(make_pair(kBigLength - 3, -1));
+  MultiTest(this->test_.NewLargeFetcher(),
+            this->test_.BigUrl(),
             ranges,
             "ijhij",
             5,
-            206);
+            kHttpResponsePartialContent);
 }
 
 TYPED_TEST(HttpFetcherTest, MultiHttpFetcherInsufficientTest) {
-  if (!this->IsMulti())
+  if (!this->test_.IsMulti())
     return;
-  typename TestFixture::HttpServer server;
-  ASSERT_TRUE(server.started_);
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
 
   vector<pair<off_t, off_t> > ranges;
-  ranges.push_back(make_pair(kBigSize - 2, 4));
+  ranges.push_back(make_pair(kBigLength - 2, 4));
   for (int i = 0; i < 2; ++i) {
     LOG(INFO) << "i = " << i;
-    MultiTest(this->NewLargeFetcher(),
-              this->BigUrl(),
+    MultiTest(this->test_.NewLargeFetcher(),
+              this->test_.BigUrl(),
               ranges,
               "ij",
               2,
-              0);
+              kHttpResponseUndefined);
     ranges.push_back(make_pair(0, 5));
   }
 }
 
+// Issue #18143: when a fetch of a secondary chunk out of a chain, then it
+// should retry with other proxies listed before giving up.
+//
+// (1) successful recovery: The offset fetch will fail twice but succeed with
+// the third proxy.
+TYPED_TEST(HttpFetcherTest, MultiHttpFetcherErrorIfOffsetRecoverableTest) {
+  if (!this->test_.IsMulti())
+    return;
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
+
+  vector<pair<off_t, off_t> > ranges;
+  ranges.push_back(make_pair(0, 25));
+  ranges.push_back(make_pair(99, -1));
+  MultiTest(this->test_.NewLargeFetcher(3),
+            LocalServerUrlForPath(base::StringPrintf("/error-if-offset/%d/2",
+                                                     kBigLength)),
+            ranges,
+            "abcdefghijabcdefghijabcdejabcdefghijabcdef",
+            kBigLength - (99 - 25),
+            kHttpResponsePartialContent);
+}
+
+// (2) unsuccessful recovery: The offset fetch will fail repeatedly.  The
+// fetcher will signal a (failed) completed transfer to the delegate.
+TYPED_TEST(HttpFetcherTest, MultiHttpFetcherErrorIfOffsetUnrecoverableTest) {
+  if (!this->test_.IsMulti())
+    return;
+
+  scoped_ptr<HttpServer> server(this->test_.CreateServer());
+  ASSERT_TRUE(server->started_);
+
+  vector<pair<off_t, off_t> > ranges;
+  ranges.push_back(make_pair(0, 25));
+  ranges.push_back(make_pair(99, -1));
+  MultiTest(this->test_.NewLargeFetcher(2),
+            LocalServerUrlForPath(base::StringPrintf("/error-if-offset/%d/3",
+                                                     kBigLength)),
+            ranges,
+            "abcdefghijabcdefghijabcde",  // only received the first chunk
+            25,
+            kHttpResponseUndefined);
+}
+
+
+
 namespace {
 class BlockedTransferTestDelegate : public HttpFetcherDelegate {
  public:
@@ -849,19 +1007,18 @@
 }  // namespace
 
 TYPED_TEST(HttpFetcherTest, BlockedTransferTest) {
-  if (this->IsMock() || this->IsMulti())
+  if (this->test_.IsMock() || this->test_.IsMulti())
     return;
 
   for (int i = 0; i < 2; i++) {
-    typename TestFixture::HttpServer server;
-
-    ASSERT_TRUE(server.started_);
+    scoped_ptr<HttpServer> server(this->test_.CreateServer());
+    ASSERT_TRUE(server->started_);
 
     GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
     BlockedTransferTestDelegate delegate;
     delegate.loop_ = loop;
 
-    scoped_ptr<HttpFetcher> fetcher(this->NewLargeFetcher());
+    scoped_ptr<HttpFetcher> fetcher(this->test_.NewLargeFetcher());
     LibcurlHttpFetcher* curl_fetcher =
         dynamic_cast<LibcurlHttpFetcher*>(fetcher.get());
     bool is_expensive_connection = (i == 0);
@@ -873,7 +1030,7 @@
     fetcher->set_delegate(&delegate);
 
     StartTransferArgs start_xfer_args =
-        { fetcher.get(), LocalServerUrlForPath(this->SmallUrl()) };
+        { fetcher.get(), LocalServerUrlForPath(this->test_.SmallUrl()) };
 
     g_timeout_add(0, StartTransfer, &start_xfer_args);
     g_main_loop_run(loop);
diff --git a/http_fetcher_unittest.h b/http_fetcher_unittest.h
new file mode 100644
index 0000000..5d17903
--- /dev/null
+++ b/http_fetcher_unittest.h
@@ -0,0 +1,17 @@
+// Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Definition of unit testing constants, to be shared by different testing
+// related modules.
+
+#ifndef CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_FETCHER_TEST_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_FETCHER_TEST_H__
+
+namespace {
+
+const int kServerPort = 8088;
+
+}  // namespace
+
+#endif  // CHROMEOS_PLATFORM_UPDATE_ENGINE_HTTP_FETCHER_TEST_H__
diff --git a/libcurl_http_fetcher.cc b/libcurl_http_fetcher.cc
index 499087f..05d03fa 100644
--- a/libcurl_http_fetcher.cc
+++ b/libcurl_http_fetcher.cc
@@ -246,24 +246,33 @@
       return;
     }
 
-    if (!sent_byte_ && ! IsHttpResponseSuccess()) {
+    if ((!sent_byte_ && !IsHttpResponseSuccess()) || IsHttpResponseError()) {
       // The transfer completed w/ error and we didn't get any bytes.
       // If we have another proxy to try, try that.
+      //
+      // TODO(garnold) in fact there are two separate cases here: one case is an
+      // other-than-success return code (including no return code) and no
+      // received bytes, which is necessary due to the way callbacks are
+      // currently processing error conditions;  the second is an explicit HTTP
+      // error code, where some data may have been received (as in the case of a
+      // semi-successful multi-chunk fetch).  This is a confusing behavior and
+      // should be unified into a complete, coherent interface.
+      LOG(INFO) << "Transfer resulted in an error (" << http_response_code_
+                << "), " << bytes_downloaded_ << " bytes downloaded";
 
       PopProxy();  // Delete the proxy we just gave up on.
 
       if (HasProxy()) {
         // We have another proxy. Retry immediately.
+        LOG(INFO) << "Trying next proxy: " << GetCurrentProxy();
         g_idle_add(&LibcurlHttpFetcher::StaticRetryTimeoutCallback, this);
       } else {
         // Out of proxies. Give up.
+        LOG(INFO) << "No further proxies, indicating transfer complete";
         if (delegate_)
-          delegate_->TransferComplete(this, false);  // success
+          delegate_->TransferComplete(this, false);  // signal fail
       }
-      return;
-    }
-
-    if ((transfer_size_ >= 0) && (bytes_downloaded_ < transfer_size_)) {
+    } else if ((transfer_size_ >= 0) && (bytes_downloaded_ < transfer_size_)) {
       // Need to restart transfer
       retry_count_++;
       LOG(INFO) << "Restarting transfer b/c we finished, had downloaded "
@@ -271,16 +280,16 @@
                 << transfer_size_ << ". retry_count: " << retry_count_;
       if (retry_count_ > kMaxRetriesCount) {
         if (delegate_)
-          delegate_->TransferComplete(this, false);  // success
+          delegate_->TransferComplete(this, false);  // signal fail
       } else {
         g_timeout_add_seconds(retry_seconds_,
                               &LibcurlHttpFetcher::StaticRetryTimeoutCallback,
                               this);
       }
-      return;
     } else {
+      LOG(INFO) << "Transfer completed (" << http_response_code_
+                << "), " << bytes_downloaded_ << " bytes downloaded";
       if (delegate_) {
-        // success is when http_response_code is 2xx
         bool success = IsHttpResponseSuccess();
         delegate_->TransferComplete(this, success);
       }
@@ -297,7 +306,7 @@
   const size_t payload_size = size * nmemb;
 
   // Do nothing if no payload or HTTP response is an error.
-  if (payload_size == 0 || ! IsHttpResponseSuccess()) {
+  if (payload_size == 0 || !IsHttpResponseSuccess()) {
     LOG(INFO) << "HTTP response unsuccessful (" << http_response_code_
               << ") or no payload (" << payload_size << "), nothing to do";
     return 0;
diff --git a/libcurl_http_fetcher.h b/libcurl_http_fetcher.h
index d727f19..d9d1697 100644
--- a/libcurl_http_fetcher.h
+++ b/libcurl_http_fetcher.h
@@ -111,11 +111,17 @@
   // Asks libcurl for the http response code and stores it in the object.
   void GetHttpResponseCode();
 
-  // Checks whether stored HTTP response is successful.
+  // Checks whether stored HTTP response is within the success range.
   inline bool IsHttpResponseSuccess() {
     return (http_response_code_ >= 200 && http_response_code_ < 300);
   }
 
+  // Checks whether stored HTTP response is within the error range. This
+  // includes both errors with the request (4xx) and server errors (5xx).
+  inline bool IsHttpResponseError() {
+    return (http_response_code_ >= 400 && http_response_code_ < 600);
+  }
+
   // Resumes a transfer where it left off. This will use the
   // HTTP Range: header to make a new connection from where the last
   // left off.
diff --git a/multi_range_http_fetcher.cc b/multi_range_http_fetcher.cc
index 8b3d8d7..40ef1e8 100644
--- a/multi_range_http_fetcher.cc
+++ b/multi_range_http_fetcher.cc
@@ -11,7 +11,7 @@
 // Begins the transfer to the specified URL.
 // State change: Stopped -> Downloading
 // (corner case: Stopped -> Stopped for an empty request)
-void MultiRangeHTTPFetcher::BeginTransfer(const std::string& url) {
+void MultiRangeHttpFetcher::BeginTransfer(const std::string& url) {
   CHECK(!base_fetcher_active_) << "BeginTransfer but already active.";
   CHECK(!pending_transfer_ended_) << "BeginTransfer but pending.";
   CHECK(!terminating_) << "BeginTransfer but terminating.";
@@ -31,7 +31,7 @@
 }
 
 // State change: Downloading -> Pending transfer ended
-void MultiRangeHTTPFetcher::TerminateTransfer() {
+void MultiRangeHttpFetcher::TerminateTransfer() {
   if (!base_fetcher_active_) {
     LOG(INFO) << "Called TerminateTransfer but not active.";
     // Note that after the callback returns this object may be destroyed.
@@ -47,7 +47,7 @@
 }
 
 // State change: Stopped or Downloading -> Downloading
-void MultiRangeHTTPFetcher::StartTransfer() {
+void MultiRangeHttpFetcher::StartTransfer() {
   if (current_index_ >= ranges_.size()) {
     return;
   }
@@ -62,7 +62,7 @@
 }
 
 // State change: Downloading -> Downloading or Pending transfer ended
-void MultiRangeHTTPFetcher::ReceivedBytes(HttpFetcher* fetcher,
+void MultiRangeHttpFetcher::ReceivedBytes(HttpFetcher* fetcher,
                                           const char* bytes,
                                           int length) {
   CHECK_LT(current_index_, ranges_.size());
@@ -92,7 +92,7 @@
 }
 
 // State change: Downloading or Pending transfer ended -> Stopped
-void MultiRangeHTTPFetcher::TransferEnded(HttpFetcher* fetcher,
+void MultiRangeHttpFetcher::TransferEnded(HttpFetcher* fetcher,
                                           bool successful) {
   CHECK(base_fetcher_active_) << "Transfer ended unexpectedly.";
   CHECK_EQ(fetcher, base_fetcher_.get());
@@ -138,18 +138,18 @@
     delegate_->TransferComplete(this, successful);
 }
 
-void MultiRangeHTTPFetcher::TransferComplete(HttpFetcher* fetcher,
+void MultiRangeHttpFetcher::TransferComplete(HttpFetcher* fetcher,
                                              bool successful) {
   LOG(INFO) << "Received transfer complete.";
   TransferEnded(fetcher, successful);
 }
 
-void MultiRangeHTTPFetcher::TransferTerminated(HttpFetcher* fetcher) {
+void MultiRangeHttpFetcher::TransferTerminated(HttpFetcher* fetcher) {
   LOG(INFO) << "Received transfer terminated.";
   TransferEnded(fetcher, false);
 }
 
-void MultiRangeHTTPFetcher::Reset() {
+void MultiRangeHttpFetcher::Reset() {
   base_fetcher_active_ = pending_transfer_ended_ = terminating_ = false;
   current_index_ = 0;
   bytes_received_this_range_ = 0;
diff --git a/multi_range_http_fetcher.h b/multi_range_http_fetcher.h
index 306ac2a..342588a 100644
--- a/multi_range_http_fetcher.h
+++ b/multi_range_http_fetcher.h
@@ -14,14 +14,14 @@
 #include "update_engine/http_fetcher.h"
 
 // This class is a simple wrapper around an HttpFetcher. The client
-// specifies a vector of byte ranges. MultiRangeHTTPFetcher will fetch bytes
+// specifies a vector of byte ranges. MultiRangeHttpFetcher will fetch bytes
 // from those offsets, using the same bash fetcher for all ranges. Thus, the
 // fetcher must support beginning a transfter after one has stopped. Pass -1
 // as a length to specify unlimited length. It really only would make sense
 // for the last range specified to have unlimited length, tho it is legal for
 // other entries to have unlimited length.
 
-// There are three states a MultiRangeHTTPFetcher object will be in:
+// There are three states a MultiRangeHttpFetcher object will be in:
 // - Stopped (start state)
 // - Downloading
 // - Pending transfer ended
@@ -30,10 +30,10 @@
 
 namespace chromeos_update_engine {
 
-class MultiRangeHTTPFetcher : public HttpFetcher, public HttpFetcherDelegate {
+class MultiRangeHttpFetcher : public HttpFetcher, public HttpFetcherDelegate {
  public:
   // Takes ownership of the passed in fetcher.
-  explicit MultiRangeHTTPFetcher(HttpFetcher* base_fetcher)
+  explicit MultiRangeHttpFetcher(HttpFetcher* base_fetcher)
       : HttpFetcher(base_fetcher->proxy_resolver()),
         base_fetcher_(base_fetcher),
         base_fetcher_active_(false),
@@ -41,7 +41,7 @@
         terminating_(false),
         current_index_(0),
         bytes_received_this_range_(0) {}
-  ~MultiRangeHTTPFetcher() {}
+  ~MultiRangeHttpFetcher() {}
 
   void ClearRanges() { ranges_.clear(); }
 
@@ -122,7 +122,7 @@
   RangesVect::size_type current_index_;  // index into ranges_
   off_t bytes_received_this_range_;
 
-  DISALLOW_COPY_AND_ASSIGN(MultiRangeHTTPFetcher);
+  DISALLOW_COPY_AND_ASSIGN(MultiRangeHttpFetcher);
 };
 
 }  // namespace chromeos_update_engine
diff --git a/proxy_resolver.cc b/proxy_resolver.cc
index 26dff7b..43754be 100644
--- a/proxy_resolver.cc
+++ b/proxy_resolver.cc
@@ -33,8 +33,10 @@
 void DirectProxyResolver::ReturnCallback(ProxiesResolvedFn callback,
                                          void* data) {
   idle_callback_id_ = 0;
-  std::deque<std::string> proxies;
-  proxies.push_back(kNoProxy);
+
+  // Initialize proxy pool with as many proxies as indicated (all identical).
+  std::deque<std::string> proxies(num_proxies_, kNoProxy);
+
   (*callback)(proxies, data);
 }
 
diff --git a/proxy_resolver.h b/proxy_resolver.h
index 53fd920..fb96073 100644
--- a/proxy_resolver.h
+++ b/proxy_resolver.h
@@ -46,16 +46,26 @@
 // Always says to not use a proxy
 class DirectProxyResolver : public ProxyResolver {
  public:
-  DirectProxyResolver() : idle_callback_id_(0) {}
+  DirectProxyResolver() : idle_callback_id_(0), num_proxies_(1) {}
   virtual ~DirectProxyResolver();
   virtual bool GetProxiesForUrl(const std::string& url,
                                 ProxiesResolvedFn callback,
                                 void* data);
 
+  // Set the number of direct (non-) proxies to be returned by resolver.
+  // The default value is 1; higher numbers are currently used in testing.
+  inline void set_num_proxies(size_t num_proxies) {
+    num_proxies_ = num_proxies;
+  }
+
  private:
   // The ID of the idle main loop callback
   guint idle_callback_id_;
 
+  // Number of direct proxies to return on resolved list; currently used for
+  // testing.
+  size_t num_proxies_;
+
   // The MainLoop callback, from here we return to the client.
   void ReturnCallback(ProxiesResolvedFn callback, void* data);
   DISALLOW_COPY_AND_ASSIGN(DirectProxyResolver);
diff --git a/test_http_server.cc b/test_http_server.cc
index 11b66ca..a6410e4 100644
--- a/test_http_server.cc
+++ b/test_http_server.cc
@@ -26,96 +26,84 @@
 #include <vector>
 
 #include <base/logging.h>
+#include <base/string_split.h>
+#include <base/string_util.h>
+
+#include "update_engine/http_common.h"
+#include "update_engine/http_fetcher_unittest.h"
+
 
 using std::min;
 using std::string;
 using std::vector;
 
+
 namespace chromeos_update_engine {
 
+// HTTP end-of-line delimiter; sorry, this needs to be a macro.
+#define EOL "\r\n"
+
 struct HttpRequest {
-  HttpRequest() : offset(0), return_code(200) {}
+  HttpRequest() : start_offset(0), return_code(kHttpResponseOk) {}
   string host;
   string url;
-  off_t offset;
-  int return_code;
+  off_t start_offset;
+  HttpResponseCode return_code;
 };
 
-namespace {
-const int kPort = 8088;  // hardcoded for now
-const int kBigLength = 100000;
-const int kMediumLength = 1000;
-}
-
 bool ParseRequest(int fd, HttpRequest* request) {
   string headers;
-  while(headers.find("\r\n\r\n") == string::npos) {
-    vector<char> buf(1024);
-    memset(&buf[0], 0, buf.size());
-    ssize_t r = read(fd, &buf[0], buf.size() - 1);
+  do {
+    char buf[1024];
+    ssize_t r = read(fd, buf, sizeof(buf));
     if (r < 0) {
       perror("read");
       exit(1);
     }
-    buf.resize(r);
+    headers.append(buf, r);
+  } while (!EndsWith(headers, EOL EOL, true));
 
-    headers.insert(headers.end(), buf.begin(), buf.end());
-  }
-  LOG(INFO) << "got headers: " << headers;
+  LOG(INFO) << "got headers:\n--8<------8<------8<------8<----\n"
+            << headers
+            << "\n--8<------8<------8<------8<----";
 
-  string::size_type url_start, url_end;
-  CHECK_NE(headers.find("GET "), string::npos);
-  url_start = headers.find("GET ") + strlen("GET ");
-  url_end = headers.find(' ', url_start);
-  CHECK_NE(string::npos, url_end);
-  string url = headers.substr(url_start, url_end - url_start);
-  LOG(INFO) << "URL: " << url;
-  request->url = url;
+  // Break header into lines.
+  std::vector<string> lines;
+  base::SplitStringUsingSubstr(
+      headers.substr(0, headers.length() - strlen(EOL EOL)), EOL, &lines);
 
-  string::size_type range_start, range_end;
-  if (headers.find("\r\nRange: ") == string::npos) {
-    request->offset = 0;
-  } else {
-    range_start = headers.find("\r\nRange: ") + strlen("\r\nRange: ");
-    range_end = headers.find('\r', range_start);
-    CHECK_NE(string::npos, range_end);
-    string range_header = headers.substr(range_start, range_end - range_start);
+  // Decode URL line.
+  std::vector<string> terms;
+  base::SplitStringAlongWhitespace(lines[0], &terms);
+  CHECK_EQ(terms.size(), 3);
+  CHECK_EQ(terms[0], "GET");
+  request->url = terms[1];
+  LOG(INFO) << "URL: " << request->url;
 
-    LOG(INFO) << "Range: " << range_header;
-    CHECK(*range_header.rbegin() == '-');
-    request->offset = atoll(range_header.c_str() + strlen("bytes="));
-    request->return_code = 206;  // Success for Range: request
-    LOG(INFO) << "Offset: " << request->offset;
-  }
+  // Decode remaining lines.
+  size_t i;
+  for (i = 1; i < lines.size(); i++) {
+    std::vector<string> terms;
+    base::SplitStringAlongWhitespace(lines[i], &terms);
 
-  if (headers.find("\r\nHost: ") == string::npos) {
-    request->host = "";
-  } else {
-    string::size_type host_start =
-        headers.find("\r\nHost: ") + strlen("\r\nHost: ");
-    string::size_type host_end = headers.find('\r', host_start);
-    CHECK_NE(string::npos, host_end);
-    string host = headers.substr(host_start, host_end - host_start);
-
-    LOG(INFO) << "Host: " << host;
-    request->host = host;
-  }
-
-  return true;
-}
-
-bool WriteString(int fd, const string& str) {
-  unsigned int bytes_written = 0;
-  while (bytes_written < str.size()) {
-    ssize_t r = write(fd, str.data() + bytes_written,
-                      str.size() - bytes_written);
-    if (r < 0) {
-      perror("write");
-      LOG(INFO) << "write failed";
-      return false;
+    if (terms[0] == "Range:") {
+      CHECK_EQ(terms.size(), 2);
+      string &range = terms[1];
+      LOG(INFO) << "range attribute: " << range;
+      CHECK(StartsWithASCII(range, "bytes=", true) &&
+            EndsWith(range, "-", true));
+      request->start_offset = atoll(range.c_str() + strlen("bytes="));
+      request->return_code = kHttpResponsePartialContent;
+      LOG(INFO) << "decoded start offset: " << request->start_offset;
+    } else if (terms[0] == "Host:") {
+      CHECK_EQ(terms.size(), 2);
+      request->host = terms[1];
+      LOG(INFO) << "host attribute: " << request->host;
+    } else {
+      LOG(WARNING) << "ignoring HTTP attribute: `" << lines[i] << "'";
     }
-    bytes_written += r;
   }
+
   return true;
 }
 
@@ -125,118 +113,194 @@
   return buf;
 }
 
-// Return a string description for common HTTP response codes.
-const char *GetHttpStatusLine(int code_id) {
-  struct HttpStatusCode {
-    int id;
-    const char* description;
-  } http_status_codes[] = {
-    { 200, "OK" },
-    { 201, "Created" },
-    { 202, "Accepted" },
-    { 203, "Non-Authoritative Information" },
-    { 204, "No Content" },
-    { 205, "Reset Content" },
-    { 206, "Partial Content" },
-    { 300, "Multiple Choices" },
-    { 301, "Moved Permanently" },
-    { 302, "Found" },
-    { 303, "See Other" },
-    { 304, "Not Modified" },
-    { 305, "Use Proxy" },
-    { 307, "Temporary Redirect" },
-    { 400, "Bad Request" },
-    { 401, "Unauthorized" },
-    { 403, "Forbidden" },
-    { 404, "Not Found" },
-    { 408, "Request Timeout" },
-    { 500, "Internal Server Error" },
-    { 501, "Not Implemented" },
-    { 503, "Service Unavailable" },
-    { 505, "HTTP Version Not Supported" },
-    { 0, "Undefined" },
-  };
+// Writes a string into a file. Returns total number of bytes written or -1 if a
+// write error occurred.
+ssize_t WriteString(int fd, const string& str) {
+  const size_t total_size = str.size();
+  size_t remaining_size = total_size;
+  char const *data = str.data();
 
-  int i;
-  for (i = 0; http_status_codes[i].id; ++i)
-    if (http_status_codes[i].id == code_id)
-      break;
-
-  return http_status_codes[i].description;
-}
-
-
-void WriteHeaders(int fd, bool support_range, off_t full_size,
-                  off_t start_offset, int return_code) {
-  LOG(INFO) << "writing headers";
-  WriteString(fd, string("HTTP/1.1 ") + Itoa(return_code) + " " +
-              GetHttpStatusLine(return_code) + "\r\n");
-  WriteString(fd, "Content-Type: application/octet-stream\r\n");
-  if (support_range) {
-    WriteString(fd, "Accept-Ranges: bytes\r\n");
-    WriteString(fd, string("Content-Range: bytes ") + Itoa(start_offset) +
-                "-" + Itoa(full_size - 1) + "/" + Itoa(full_size) + "\r\n");
+  while (remaining_size) {
+    ssize_t written = write(fd, data, remaining_size);
+    if (written < 0) {
+      perror("write");
+      LOG(INFO) << "write failed";
+      return -1;
+    }
+    data += written;
+    remaining_size -= written;
   }
-  off_t content_length = full_size;
-  if (support_range)
-    content_length -= start_offset;
-  WriteString(fd, string("Content-Length: ") + Itoa(content_length) + "\r\n");
-  WriteString(fd, "\r\n");
+
+  return total_size;
 }
 
-void HandleQuitQuitQuit(int fd) {
-  WriteHeaders(fd, true, 0, 0, 200);
+// Writes the headers of an HTTP response into a file.
+ssize_t WriteHeaders(int fd, const off_t start_offset, const off_t end_offset,
+                     HttpResponseCode return_code) {
+  ssize_t written = 0, ret;
+
+  ret = WriteString(fd,
+                    string("HTTP/1.1 ") + Itoa(return_code) + " " +
+                    GetHttpResponseDescription(return_code) +
+                    EOL
+                    "Content-Type: application/octet-stream" EOL);
+  if (ret < 0)
+    return -1;
+  written += ret;
+
+  off_t content_length = end_offset;
+  if (start_offset) {
+    ret = WriteString(fd,
+                      string("Accept-Ranges: bytes" EOL
+                             "Content-Range: bytes ") +
+                      Itoa(start_offset) + "-" + Itoa(end_offset - 1) + "/" +
+                      Itoa(end_offset) + EOL);
+    if (ret < 0)
+      return -1;
+    written += ret;
+
+    content_length -= start_offset;
+  }
+
+  ret = WriteString(fd, string("Content-Length: ") + Itoa(content_length) +
+                    EOL EOL);
+  if (ret < 0)
+    return -1;
+  written += ret;
+
+  return written;
+}
+
+// Writes a predetermined payload of lines of ascending bytes to a file. The
+// first byte of output is appropriately offset with respect to the request line
+// length.  Returns the number of successfully written bytes.
+size_t WritePayload(int fd, const off_t start_offset, const off_t end_offset,
+                    const char first_byte, const size_t line_len) {
+  CHECK_LE(start_offset, end_offset);
+  CHECK_GT(line_len, 0);
+
+  LOG(INFO) << "writing payload: " << line_len << " byte lines starting with `"
+            << first_byte << "', offset range " << start_offset << " -> "
+            << end_offset;
+
+  // Populate line of ascending characters.
+  string line;
+  line.reserve(line_len);
+  char byte = first_byte;
+  size_t i;
+  for (i = 0; i < line_len; i++)
+    line += byte++;
+
+  const size_t total_len = end_offset - start_offset;
+  size_t remaining_len = total_len;
+  bool success = true;
+
+  // If start offset is not aligned with line boundary, output partial line up
+  // to the first line boundary.
+  size_t start_modulo = start_offset % line_len;
+  if (start_modulo) {
+    string partial = line.substr(start_modulo, remaining_len);
+    ssize_t ret = WriteString(fd, partial);
+    if ((success = (ret >= 0 && (size_t) ret == partial.length())))
+      remaining_len -= partial.length();
+  }
+
+  // Output full lines up to the maximal line boundary below the end offset.
+  while (success && remaining_len >= line_len) {
+    ssize_t ret = WriteString(fd, line);
+    if ((success = (ret >= 0 && (size_t) ret == line_len)))
+      remaining_len -= line_len;
+  }
+
+  // Output a partial line up to the end offset.
+  if (success && remaining_len) {
+    string partial = line.substr(0, remaining_len);
+    ssize_t ret = WriteString(fd, partial);
+    if ((success = (ret >= 0 && (size_t) ret == partial.length())))
+      remaining_len -= partial.length();
+  }
+
+  return (total_len - remaining_len);
+}
+
+// Write default payload lines of the form 'abcdefghij'.
+inline size_t WritePayload(int fd, const off_t start_offset,
+                           const off_t end_offset) {
+  return WritePayload(fd, start_offset, end_offset, 'a', 10);
+}
+
+// Send an empty response, then kill the server.
+void HandleQuit(int fd) {
+  WriteHeaders(fd, 0, 0, kHttpResponseOk);
   exit(0);
 }
 
-void HandleBig(int fd, const HttpRequest& request, int big_length) {
-  LOG(INFO) << "starting big";
-  const off_t full_length = big_length;
-  WriteHeaders(fd, true, full_length, request.offset, request.return_code);
-  int i = request.offset;
-  bool success = true;
-  for (; (i % 10) && success; i++)
-    success = WriteString(fd, string(1, 'a' + (i % 10)));
-  if (success)
-    CHECK_EQ(i % 10, 0);
-  for (; (i < full_length) && success; i += 10) {
-    success = WriteString(fd, "abcdefghij");
-  }
-  if (success)
-    CHECK_EQ(i, full_length);
-  LOG(INFO) << "Done w/ big";
+
+// Generates an HTTP response with payload corresponding to given offset and
+// length.  Returns the total number of bytes delivered or -1 for error.
+ssize_t HandleGet(int fd, const HttpRequest& request, const off_t end_offset) {
+  LOG(INFO) << "starting payload";
+  ssize_t written = 0, ret;
+
+  if ((ret = WriteHeaders(fd, request.start_offset, end_offset,
+                          request.return_code)) < 0)
+    return -1;
+  written += ret;
+
+  if ((ret = WritePayload(fd, request.start_offset, end_offset)) < 0)
+    return -1;
+  written += ret;
+
+  LOG(INFO) << "payload writing completed, " << written << " bytes written";
+  return written;
 }
 
-// This is like /big, but it writes at most 9000 bytes. Also,
-// half way through it sleeps for 70 seconds
-// (technically, when (offset % (9000 * 7)) == 0).
-void HandleFlaky(int fd, const HttpRequest& request) {
-  const off_t full_length = kBigLength;
-  WriteHeaders(fd, true, full_length, request.offset, request.return_code);
-  const off_t content_length =
-      min(static_cast<off_t>(9000), full_length - request.offset);
-  const bool should_sleep = (request.offset % (9000 * 7)) == 0;
 
-  string buf;
+// Generates an HTTP response with payload. Payload is truncated at a given
+// length. Transfer may include an optional delay midway.
+ssize_t HandleFlakyGet(int fd, const HttpRequest& request,
+                       const off_t max_end_offset, const off_t truncate_length,
+                       const int sleep_every, const int sleep_secs) {
+  CHECK_GT(truncate_length, 0);
+  CHECK_GT(sleep_every, 0);
+  CHECK_GE(sleep_secs, 0);
 
-  for (int i = request.offset; i % 10; i++)
-    buf.append(1, 'a' + (i % 10));
-  while (static_cast<off_t>(buf.size()) < content_length)
-    buf.append("abcdefghij");
-  buf.resize(content_length);
+  ssize_t ret;
+  size_t written = 0;
+  const off_t start_offset = request.start_offset;
 
-  if (!should_sleep) {
-    LOG(INFO) << "writing data blob of size " << buf.size();
-    WriteString(fd, buf);
-  } else {
-    string::size_type half_way_point = buf.size() / 2;
-    LOG(INFO) << "writing small data blob of size " << half_way_point;
-    WriteString(fd, buf.substr(0, half_way_point));
-    sleep(10);
+  if ((ret = WriteHeaders(fd, start_offset, max_end_offset,
+                          request.return_code)) < 0)
+    return -1;
+
+  const size_t content_length =
+      min(truncate_length, max_end_offset - start_offset);
+  const off_t end_offset = start_offset + content_length;
+
+  if (start_offset % (truncate_length * sleep_every) == 0) {
+    const size_t midway_content_length = content_length / 2;
+    const off_t midway_offset = start_offset + midway_content_length;
+
+    LOG(INFO) << "writing small data blob of size " << midway_content_length;
+    if ((ret = WritePayload(fd, start_offset, midway_offset)) < 0)
+      return -1;
+    written += ret;
+
+    sleep(sleep_secs);
+
     LOG(INFO) << "writing small data blob of size "
-              << (buf.size() - half_way_point);
-    WriteString(fd, buf.substr(half_way_point, buf.size() - half_way_point));
+              << (content_length - midway_content_length);
+    if ((ret = WritePayload(fd, midway_offset, end_offset)) < 0)
+      return -1;
+    written += ret;
+  } else {
+    LOG(INFO) << "writing data blob of size " << content_length;
+    if ((ret = WritePayload(fd, start_offset, end_offset)) < 0)
+      return -1;
+    written += ret;
   }
+
+  return written;
 }
 
 // Handles /redirect/<code>/<url> requests by returning the specified
@@ -248,63 +312,144 @@
   url.erase(0, strlen("/redirect/"));
   string::size_type url_start = url.find('/');
   CHECK_NE(url_start, string::npos);
-  string code = url.substr(0, url_start);
+  HttpResponseCode code = StringToHttpResponseCode(url.c_str());
   url.erase(0, url_start);
   url = "http://" + request.host + url;
-  string status;
-  if (code == "301") {
-    status = "Moved Permanently";
-  } else if (code == "302") {
-    status = "Found";
-  } else if (code == "303") {
-    status = "See Other";
-  } else if (code == "307") {
-    status = "Temporary Redirect";
-  } else {
+  const char *status = GetHttpResponseDescription(code);
+  if (!status)
     CHECK(false) << "Unrecognized redirection code: " << code;
-  }
   LOG(INFO) << "Code: " << code << " " << status;
   LOG(INFO) << "New URL: " << url;
-  WriteString(fd, "HTTP/1.1 " + code + " " + status + "\r\n");
-  WriteString(fd, "Location: " + url + "\r\n");
+
+  ssize_t ret;
+  if ((ret = WriteString(fd, "HTTP/1.1 " + Itoa(code) + " " +
+                         status + EOL)) < 0)
+    return;
+  WriteString(fd, "Location: " + url + EOL);
+}
+
+// Generate a page not found error response with actual text payload. Return
+// number of bytes written or -1 for error.
+ssize_t HandleError(int fd, const HttpRequest& request) {
+  LOG(INFO) << "Generating error HTTP response";
+
+  ssize_t ret;
+  size_t written = 0;
+
+  const string data("This is an error page.");
+
+  if ((ret = WriteHeaders(fd, 0, data.size(), kHttpResponseNotFound)) < 0)
+    return -1;
+  written += ret;
+
+  if ((ret = WriteString(fd, data)) < 0)
+    return -1;
+  written += ret;
+
+  return written;
+}
+
+// Generate an error response if the requested offset is nonzero, up to a given
+// maximal number of successive failures.  The error generated is an "Internal
+// Server Error" (500).
+ssize_t HandleErrorIfOffset(int fd, const HttpRequest& request,
+                            size_t end_offset, int max_fails) {
+  static int num_fails = 0;
+
+  if (request.start_offset > 0 && num_fails < max_fails) {
+    LOG(INFO) << "Generating error HTTP response";
+
+    ssize_t ret;
+    size_t written = 0;
+
+    const string data("This is an error page.");
+
+    if ((ret = WriteHeaders(fd, 0, data.size(),
+                            kHttpResponseInternalServerError)) < 0)
+      return -1;
+    written += ret;
+
+    if ((ret = WriteString(fd, data)) < 0)
+      return -1;
+    written += ret;
+
+    num_fails++;
+    return written;
+  } else {
+    num_fails = 0;
+    return HandleGet(fd, request, end_offset);
+  }
 }
 
 void HandleDefault(int fd, const HttpRequest& request) {
+  const off_t start_offset = request.start_offset;
   const string data("unhandled path");
-  WriteHeaders(fd, true, data.size(), request.offset, request.return_code);
-  const string data_to_write(data.substr(request.offset,
-                                         data.size() - request.offset));
-  WriteString(fd, data_to_write);
+  const size_t size = data.size();
+  ssize_t ret;
+
+  if ((ret = WriteHeaders(fd, start_offset, size, request.return_code)) < 0)
+    return;
+  WriteString(fd, (start_offset < static_cast<off_t>(size) ?
+                   data.substr(start_offset) : ""));
 }
 
-// Handle an error response from server.
-void HandleError(int fd, const HttpRequest& request) {
-  LOG(INFO) << "Generating error HTTP response";
 
-  // Generate a Not Found" error page with some text.
-  const string data("This is an error page.");
-  WriteHeaders(fd, false, data.size(), 0, 404);
-  WriteString(fd, data);
-}
+// Break a URL into terms delimited by slashes.
+class UrlTerms {
+ public:
+  UrlTerms(string &url, size_t num_terms) {
+    // URL must be non-empty and start with a slash.
+    CHECK_GT(url.size(), 0);
+    CHECK_EQ(url[0], '/');
+
+    // Split it into terms delimited by slashes, omitting the preceeding slash.
+    base::SplitStringDontTrim(url.substr(1), '/', &terms);
+
+    // Ensure expected length.
+    CHECK_EQ(terms.size(), num_terms);
+  }
+
+  inline string Get(const off_t index) const {
+    return terms[index];
+  }
+  inline const char *GetCStr(const off_t index) const {
+    return Get(index).c_str();
+  }
+  inline int GetInt(const off_t index) const {
+    return atoi(GetCStr(index));
+  }
+  inline long GetLong(const off_t index) const {
+    return atol(GetCStr(index));
+  }
+
+ private:
+  std::vector<string> terms;
+};
 
 void HandleConnection(int fd) {
   HttpRequest request;
   ParseRequest(fd, &request);
 
-  if (request.url == "/quitquitquit")
-    HandleQuitQuitQuit(fd);
-  else if (request.url == "/big")
-    HandleBig(fd, request, kBigLength);
-  else if (request.url == "/medium")
-    HandleBig(fd, request, kMediumLength);
-  else if (request.url == "/flaky")
-    HandleFlaky(fd, request);
-  else if (request.url.find("/redirect/") == 0)
+  string &url = request.url;
+  if (url == "/quitquitquit") {
+    HandleQuit(fd);
+  } else if (StartsWithASCII(url, "/download/", true)) {
+    const UrlTerms terms(url, 2);
+    HandleGet(fd, request, terms.GetLong(1));
+  } else if (StartsWithASCII(url, "/flaky/", true)) {
+    const UrlTerms terms(url, 5);
+    HandleFlakyGet(fd, request, terms.GetLong(1), terms.GetLong(2),
+                   terms.GetLong(3), terms.GetLong(4));
+  } else if (url.find("/redirect/") == 0) {
     HandleRedirect(fd, request);
-  else if (request.url.find("/error") == 0)
+  } else if (url == "/error") {
     HandleError(fd, request);
-  else
+  } else if (StartsWithASCII(url, "/error-if-offset/", true)) {
+    const UrlTerms terms(url, 3);
+    HandleErrorIfOffset(fd, request, terms.GetLong(1), terms.GetInt(2));
+  } else {
     HandleDefault(fd, request);
+  }
 
   close(fd);
 }
@@ -329,7 +474,7 @@
 
   server_addr.sin_family = AF_INET;
   server_addr.sin_addr.s_addr = INADDR_ANY;
-  server_addr.sin_port = htons(kPort);
+  server_addr.sin_port = htons(kServerPort);
 
   {
     // Get rid of "Address in use" error
diff --git a/update_attempter.cc b/update_attempter.cc
index 4eb2d87..7ec14d0 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -220,7 +220,7 @@
   download_fetcher->set_check_certificate(CertificateChecker::kDownload);
   shared_ptr<DownloadAction> download_action(
       new DownloadAction(prefs_,
-                         new MultiRangeHTTPFetcher(
+                         new MultiRangeHttpFetcher(
                              download_fetcher)));  // passes ownership
   shared_ptr<OmahaRequestAction> download_finished_action(
       new OmahaRequestAction(prefs_,
@@ -663,8 +663,8 @@
 }
 
 void UpdateAttempter::SetupDownload() {
-  MultiRangeHTTPFetcher* fetcher =
-      dynamic_cast<MultiRangeHTTPFetcher*>(download_action_->http_fetcher());
+  MultiRangeHttpFetcher* fetcher =
+      dynamic_cast<MultiRangeHttpFetcher*>(download_action_->http_fetcher());
   fetcher->ClearRanges();
   if (response_handler_action_->install_plan().is_resume) {
     // Resuming an update so fetch the update manifest metadata first.