diff --git a/HTTP.cabal b/HTTP.cabal
--- a/HTTP.cabal
+++ b/HTTP.cabal
@@ -1,5 +1,5 @@
 Name: HTTP
-Version: 4000.2.4
+Version: 4000.2.5
 Cabal-Version: >= 1.8
 Build-type: Simple
 License: BSD3
@@ -59,6 +59,10 @@
   default:     False
   description: Build with warnings-as-errors
 
+Flag network23
+  description: Use version 2.3.x or below of the network package
+  default: False
+
 Library
   Exposed-modules: 
                  Network.BufferType,
@@ -81,7 +85,7 @@
                  Network.HTTP.Utils
                  Paths_HTTP
   GHC-options: -fwarn-missing-signatures -Wall
-  Build-depends: base >= 2 && < 4.7, network, parsec
+  Build-depends: base >= 2 && < 4.7, parsec
   Extensions: FlexibleInstances
   if flag(old-base)
     Build-depends: base < 3
@@ -92,6 +96,11 @@
     CPP-Options: -DMTL1
   else
     Build-depends: mtl >= 2.0 && < 2.2
+  if flag(network23)
+    Build-depends: network < 2.4
+    CPP-Options: -DNETWORK23
+  else
+    Build-depends: network >= 2.4 && < 2.5
 
   if flag(warn-as-error)
     ghc-options:      -Werror
@@ -120,7 +129,7 @@
                      warp >= 1.2 && < 1.3,
                      pureMD5 >= 2.1 && < 2.2,
                      base >= 2 && < 4.6,
-                     network,
+                     network < 2.5,
                      split >= 0.1 && < 0.2,
                      test-framework,
                      test-framework-hunit
diff --git a/Network/Browser.hs b/Network/Browser.hs
--- a/Network/Browser.hs
+++ b/Network/Browser.hs
@@ -913,7 +913,7 @@
 	         		   }
                              rq2
                  where
-                   newURI_abs = maybe newURI id (newURI `relativeTo` uri)
+                   newURI_abs = uriDefaultTo newURI uri
 
       (3,0,5) ->
         case retrieveHeaders HdrLocation rsp of
@@ -1035,7 +1035,11 @@
 -- If the second argument is not sufficient context for determining
 -- a full URI then anarchy reins.
 uriDefaultTo :: URI -> URI -> URI
+#ifdef NETWORK23
 uriDefaultTo a b = maybe a id (a `relativeTo` b)
+#else
+uriDefaultTo a b = a `relativeTo` b
+#endif
 
 
 -- This form junk is completely untested...
diff --git a/Network/HTTP/Auth.hs b/Network/HTTP/Auth.hs
--- a/Network/HTTP/Auth.hs
+++ b/Network/HTTP/Auth.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 -----------------------------------------------------------------------------
 -- |
 -- Module      :  Network.HTTP.Auth
@@ -189,7 +190,11 @@
                }
 
         annotateURIs :: [Maybe URI] -> [URI]
+#ifdef NETWORK23
         annotateURIs = (map (\u -> fromMaybe u (u `relativeTo` baseURI))) . catMaybes
+#else
+        annotateURIs = map (`relativeTo` baseURI) . catMaybes
+#endif
 
         -- Change These:
         readQop :: String -> [Qop]
diff --git a/test/httpTests.hs b/test/httpTests.hs
new file mode 100644
--- /dev/null
+++ b/test/httpTests.hs
@@ -0,0 +1,627 @@
+{-# LANGUAGE ImplicitParams, ViewPatterns, NoMonomorphismRestriction #-}
+import Control.Concurrent
+
+import Control.Applicative ((<$))
+import Control.Concurrent (threadDelay)
+import Control.Exception (try)
+import qualified Data.ByteString.Lazy.Char8 as BL (pack)
+import Data.Char (isSpace)
+import qualified Data.Digest.Pure.MD5 as MD5 (md5)
+import Data.List.Split (splitOn)
+import Data.Maybe (fromJust)
+import System.IO.Error (userError)
+
+import qualified Httpd
+
+import Network.Browser
+import Network.HTTP
+import Network.HTTP.Base
+import Network.HTTP.Auth
+import Network.HTTP.Headers
+import Network.Stream (Result)
+import Network.URI (uriPath, parseURI)
+
+import System.Environment (getArgs)
+import System.IO (getChar)
+
+import Test.Framework (defaultMainWithArgs, testGroup)
+import Test.Framework.Providers.HUnit
+import Test.HUnit
+
+
+basicGetRequest :: (?testUrl :: ServerAddress) => Assertion
+basicGetRequest = do
+  response <- simpleHTTP (getRequest (?testUrl "/basic/get"))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" "It works." body
+
+basicGetRequestLBS :: (?testUrl :: ServerAddress) => Assertion
+basicGetRequestLBS = do
+  response <- simpleHTTP (mkRequest GET (fromJust (parseURI (?testUrl ("/basic/get")))))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" (BL.pack "It works.") body
+
+basicHeadRequest :: (?testUrl :: ServerAddress) => Assertion
+basicHeadRequest = do
+  response <- simpleHTTP (headRequest (?testUrl "/basic/head"))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  -- the body should be empty, since this is a HEAD request
+  assertEqual "Receiving expected response" "" body
+
+basicExample :: (?testUrl :: ServerAddress) => Assertion
+basicExample = do
+  result <-
+    -- sample code from Network.HTTP haddock, with URL changed
+    -- Note there's also a copy of the example in the .cabal file
+    simpleHTTP (getRequest (?testUrl "/basic/example")) >>= fmap (take 100) . getResponseBody
+  assertEqual "Receiving expected response" (take 100 haskellOrgText) result
+
+secureGetRequest :: (?secureTestUrl :: ServerAddress) => Assertion
+secureGetRequest = do
+  response <- try $ simpleHTTP (getRequest (?secureTestUrl "/anything"))
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show response) -- fmap show because Response isn't in Eq
+
+basicPostRequest :: (?testUrl :: ServerAddress) => Assertion
+basicPostRequest = do
+  let sendBody = "body"
+  response <- simpleHTTP $ postRequestWithBody (?testUrl "/basic/post")
+                                               "text/plain"
+                                               sendBody
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response"
+              (show (Just "text/plain", Just "4", sendBody))
+              body
+
+basicAuthFailure :: (?testUrl :: ServerAddress) => Assertion
+basicAuthFailure = do
+  response <- simpleHTTP (getRequest (?testUrl "/auth/basic"))
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "HTTP status code" ((4, 0, 1), "Nothing") (code, body)
+
+credentialsBasic :: (?testUrl :: ServerAddress) => Authority
+credentialsBasic = AuthBasic "Testing realm" "test" "password"
+                             (fromJust . parseURI . ?testUrl $ "/auth/basic")
+
+basicAuthSuccess :: (?testUrl :: ServerAddress) => Assertion
+basicAuthSuccess = do
+  let req = getRequest (?testUrl "/auth/basic")
+  let authString = withAuthority credentialsBasic req
+  let reqWithAuth = req { rqHeaders = mkHeader HdrAuthorization authString:rqHeaders req }
+  response <- simpleHTTP reqWithAuth
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" ((2, 0, 0), "Here's the secret") (code, body)
+
+browserExample :: (?testUrl :: ServerAddress) => Assertion
+browserExample = do
+  result <-
+    -- sample code from Network.Browser haddock, with URL changed
+    -- Note there's also a copy of the example in the .cabal file
+    do 
+      (_, rsp)
+         <- Network.Browser.browse $ do
+               setAllowRedirects True -- handle HTTP redirects
+               request $ getRequest (?testUrl "/browser/example")
+      return (take 100 (rspBody rsp))
+  assertEqual "Receiving expected response" (take 100 haskellOrgText) result
+
+-- A vanilla HTTP request using Browser shouln't send a cookie header
+browserNoCookie :: (?testUrl :: ServerAddress) => Assertion
+browserNoCookie = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    request $ getRequest (?testUrl "/browser/no-cookie")
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+
+-- Regression test
+--  * Browser sends vanilla request to server
+--  * Server sets one cookie "hello=world"
+--  * Browser sends a second request
+--
+-- Expected: Server gets single cookie with "hello=world"
+-- Actual:   Server gets 3 extra cookies, which are actually cookie attributes:
+--           "$Version=0;hello=world;$Domain=localhost:8080\r"
+browserOneCookie :: (?testUrl :: ServerAddress) => Assertion
+browserOneCookie = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+    -- This first requests returns a single Set-Cookie: hello=world
+    _ <- request $ getRequest (?testUrl "/browser/one-cookie/1")
+
+    -- This second request should send a single Cookie: hello=world
+    request $ getRequest (?testUrl "/browser/one-cookie/2")
+  let body = rspBody response
+  assertEqual "Receiving expected response" "" body
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+browserTwoCookies :: (?testUrl :: ServerAddress) => Assertion
+browserTwoCookies = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+    -- This first request returns two cookies
+    _ <- request $ getRequest (?testUrl "/browser/two-cookies/1")
+
+    -- This second request should send them back
+    request $ getRequest (?testUrl "/browser/two-cookies/2")
+  let body = rspBody response
+  assertEqual "Receiving expected response" "" body
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+
+browserFollowsRedirect :: (?testUrl :: ServerAddress) => Int -> Assertion
+browserFollowsRedirect n = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+    request $ getRequest (?testUrl "/browser/redirect/relative/" ++ show n ++ "/basic/get")
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "It works.")
+              (rspCode response, rspBody response)
+
+browserReturnsRedirect :: (?testUrl :: ServerAddress) => Int -> Assertion
+browserReturnsRedirect n = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+    request $ getRequest (?testUrl "/browser/redirect/relative/" ++ show n ++ "/basic/get")
+  assertEqual "Receiving expected response from server"
+              ((n `div` 100, n `mod` 100 `div` 10, n `mod` 10), "")
+              (rspCode response, rspBody response)
+
+authGenBasic _ "Testing realm" = return $ Just ("test", "password")
+authGenBasic _ realm = fail $ "Unexpected realm " ++ realm
+
+browserBasicAuth :: (?testUrl :: ServerAddress) => Assertion
+browserBasicAuth = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+
+    setAuthorityGen authGenBasic
+
+    request $ getRequest (?testUrl "/auth/basic")
+
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "Here's the secret")
+              (rspCode response, rspBody response)
+
+authGenDigest _ "Digest testing realm" = return $ Just ("test", "digestpassword")
+authGenDigest _ realm = fail $ "Unexpected digest realm " ++ realm
+
+browserDigestAuth :: (?testUrl :: ServerAddress) => Assertion
+browserDigestAuth = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    setMaxPoolSize (Just 0) -- TODO remove this: workaround for github issue 14
+
+    setAuthorityGen authGenDigest
+
+    request $ getRequest (?testUrl "/auth/digest")
+
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "Here's the digest secret")
+              (rspCode response, rspBody response)
+
+
+
+browserAlt :: (?altTestUrl :: ServerAddress) => Assertion
+browserAlt = do
+  (response) <- browse $ do
+
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?altTestUrl "/basic/get")
+
+    return response1
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response, rspBody response)
+
+-- test that requests to multiple servers on the same host
+-- don't get confused with each other
+browserBoth :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserBoth = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+-- test that requests to multiple servers on the same host
+-- don't get confused with each other
+browserBothReversed :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserBothReversed = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+browserSecureRequest :: (?secureTestUrl :: ServerAddress) => Assertion
+browserSecureRequest = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+
+    request $ getRequest (?secureTestUrl "/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+-- in case it tries to reuse the connection
+browserSecureRequestAfterInsecure :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress) => Assertion
+browserSecureRequestAfterInsecure = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+
+    request $ getRequest (?testUrl "/basic/get")
+    request $ getRequest (?secureTestUrl "/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+browserRedirectToSecure :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress) => Assertion
+browserRedirectToSecure = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+    setErrHandler fail
+
+    request $ getRequest (?testUrl "/browser/redirect/secure/301/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError $ "Unable to handle redirect, unsupported scheme: " ++ ?secureTestUrl "/anything"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+browserTwoRequests :: (?testUrl :: ServerAddress) => Assertion
+browserTwoRequests = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?testUrl "/basic/get2")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works (2).")
+              (rspCode response2, rspBody response2)
+
+
+browserTwoRequestsAlt :: (?altTestUrl :: ServerAddress) => Assertion
+browserTwoRequestsAlt = do
+  (response1, response2) <- browse $ do
+
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get2")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server (2).")
+              (rspCode response2, rspBody response2)
+
+browserTwoRequestsBoth :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserTwoRequestsBoth = do
+  (response1, response2, response3, response4) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response3) <- request $ getRequest (?testUrl "/basic/get2")
+    (_, response4) <- request $ getRequest (?altTestUrl "/basic/get2")
+
+    return (response1, response2, response3, response4)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works (2).")
+              (rspCode response3, rspBody response3)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server (2).")
+              (rspCode response4, rspBody response4)
+
+hasPrefix :: String -> String -> Maybe String
+hasPrefix [] ys = Just ys
+hasPrefix (x:xs) (y:ys) | x == y = hasPrefix xs ys
+hasPrefix _ _ = Nothing
+
+maybeRead :: Read a => String -> Maybe a
+maybeRead s =
+   case reads s of
+     [(v, "")] -> Just v
+     _ -> Nothing
+
+splitFields = map (toPair '=' . trim isSpace) . splitOn ","
+
+toPair c str = case break (==c) str of
+                 (left, _:right) -> (left, right)
+                 _ -> error $ "No " ++ show c ++ " in " ++ str
+trim f = dropWhile f . reverse . dropWhile f . reverse
+
+isSubsetOf xs ys = all (`elem` ys) xs
+
+-- first bits of result text from haskell.org (just to give some representative text)
+haskellOrgText =
+  "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\
+\<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\" dir=\"ltr\">\
+\\t<head>\
+\\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\
+\\t\t\t\t<meta name=\"keywords\" content=\"Haskell,Applications and libraries,Books,Foreign Function Interface,Functional programming,Hac Boston,HakkuTaikai,HaskellImplementorsWorkshop/2011,Haskell Communities and Activities Report,Haskell in education,Haskell in industry\" />"
+
+digestMatch
+  username realm password
+  nonce opaque
+  method relativeURI makeAbsolute
+  headers
+  =
+  common `isSubsetOf` headers && (relative `isSubsetOf` headers || absolute `isSubsetOf` headers)
+ where
+   common = [("username", show username), ("realm", show realm), ("nonce", show nonce),
+             ("opaque", show opaque)]
+   md5 = show . MD5.md5 . BL.pack
+   ha1 = md5 (username++":"++realm++":"++password)
+   ha2 uri = md5 (method++":"++uri)
+   response uri = md5 (ha1 ++ ":" ++ nonce ++ ":" ++ ha2 uri)
+   mkUncommon uri hash = [("uri", show uri), ("response", show hash)]
+   relative = mkUncommon relativeURI (response relativeURI)
+   absoluteURI = makeAbsolute relativeURI
+   absolute = mkUncommon absoluteURI (response absoluteURI)
+
+processRequest :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress)
+               => Httpd.Request
+               -> IO Httpd.Response
+processRequest req = do
+  case (Httpd.reqMethod req, Network.URI.uriPath (Httpd.reqURI req)) of 
+    ("GET", "/basic/get") -> return $ Httpd.mkResponse 200 [] "It works."
+    ("GET", "/basic/get2") -> return $ Httpd.mkResponse 200 [] "It works (2)."
+    ("GET", "/basic/head") -> return $ Httpd.mkResponse 200 [] "Body for /basic/head."
+    ("HEAD", "/basic/head") -> return $ Httpd.mkResponse 200 [] "Body for /basic/head."
+    ("POST", "/basic/post") ->
+        let typ = lookup "Content-Type" (Httpd.reqHeaders req)
+            len = lookup "Content-Length" (Httpd.reqHeaders req)
+            body = Httpd.reqBody req
+        in return $ Httpd.mkResponse 200 [] (show (typ, len, body))
+
+    ("GET", "/basic/example") ->
+      return $ Httpd.mkResponse 200 [] haskellOrgText
+
+    ("GET", "/auth/basic") ->
+      case lookup "Authorization" (Httpd.reqHeaders req) of
+        Just "Basic dGVzdDpwYXNzd29yZA==" -> return $ Httpd.mkResponse 200 [] "Here's the secret"
+        x -> return $ Httpd.mkResponse 401 [("WWW-Authenticate", "Basic realm=\"Testing realm\"")] (show x)
+
+    ("GET", "/auth/digest") ->
+      case lookup "Authorization" (Httpd.reqHeaders req) of
+        Just (hasPrefix "Digest " -> Just (splitFields -> items))
+          | digestMatch "test" "Digest testing realm" "digestpassword"
+                        "87e4" "057d"
+                        "GET" "/auth/digest" ?testUrl
+                        items
+          -> return $ Httpd.mkResponse 200 [] "Here's the digest secret"
+        x -> return $ Httpd.mkResponse
+                        401
+                        [("WWW-Authenticate",
+                          "Digest realm=\"Digest testing realm\", opaque=\"057d\", nonce=\"87e4\"")]
+                        (show x)
+
+    ("GET", "/browser/example") ->
+      return $ Httpd.mkResponse 200 [] haskellOrgText
+    ("GET", "/browser/no-cookie") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        Nothing -> return $ Httpd.mkResponse 200 [] ""
+        Just s  -> return $ Httpd.mkResponse 500 [] s
+    ("GET", "/browser/one-cookie/1") ->
+      return $ Httpd.mkResponse 200 [("Set-Cookie", "hello=world")] ""
+    ("GET", "/browser/one-cookie/2") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        Just "hello=world" -> return $ Httpd.mkResponse 200 [] ""
+        Just s               -> return $ Httpd.mkResponse 500 [] s
+        Nothing              -> return $ Httpd.mkResponse 500 [] (show $ Httpd.reqHeaders req)
+    ("GET", "/browser/two-cookies/1") ->
+      return $ Httpd.mkResponse 200
+                              [("Set-Cookie", "hello=world")
+                              ,("Set-Cookie", "goodbye=cruelworld")]
+                              ""
+    ("GET", "/browser/two-cookies/2") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        -- TODO generalise the cookie parsing to allow for whitespace/ordering variations
+        Just "goodbye=cruelworld; hello=world" -> return $ Httpd.mkResponse 200 [] ""
+        Just s               -> return $ Httpd.mkResponse 500 [] s
+        Nothing              -> return $ Httpd.mkResponse 500 [] (show $ Httpd.reqHeaders req)
+    ("GET", hasPrefix "/browser/redirect/relative/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", rest)] ""
+    ("GET", hasPrefix "/browser/redirect/absolute/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", ?testUrl rest)] ""
+    ("GET", hasPrefix "/browser/redirect/secure/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", ?secureTestUrl rest)] ""
+    _                     -> return $ Httpd.mkResponse 500 [] "Unknown request"
+
+altProcessRequest :: Httpd.Request -> IO Httpd.Response
+altProcessRequest req = do
+  case (Httpd.reqMethod req, Network.URI.uriPath (Httpd.reqURI req)) of 
+    ("GET", "/basic/get") -> return $ Httpd.mkResponse 200 [] "This is the alternate server."
+    ("GET", "/basic/get2") -> return $ Httpd.mkResponse 200 [] "This is the alternate server (2)."
+    _                     -> return $ Httpd.mkResponse 500 [] "Unknown request"
+
+maybeTestGroup True name xs = testGroup name xs
+maybeTestGroup False name _ = testGroup name []
+
+basicTests =
+    testGroup "Basic tests"
+    [ testCase "Basic GET request" basicGetRequest
+    , testCase "Basic GET request (lazy bytestring)" basicGetRequestLBS
+    , testCase "Network.HTTP example code" basicExample
+    , testCase "Secure GET request" secureGetRequest
+    , testCase "Basic POST request" basicPostRequest
+    , testCase "Basic HEAD request" basicHeadRequest
+    , testCase "Basic Auth failure" basicAuthFailure
+    , testCase "Basic Auth success" basicAuthSuccess
+    ]
+
+browserTests =
+    testGroup "Browser tests"
+    [ testGroup "Basic"
+      [
+        -- github issue 14
+        -- testCase "Two requests" browserTwoRequests
+        testCase "Network.Browser example code" browserExample
+      ]
+    , testGroup "Secure"
+      [
+        testCase "Secure request" browserSecureRequest
+      , testCase "After insecure" browserSecureRequestAfterInsecure
+      , testCase "Redirection" browserRedirectToSecure
+      ]
+    , testGroup "Cookies"
+      [ testCase "No cookie header" browserNoCookie
+      , testCase "One cookie" browserOneCookie
+      , testCase "Two cookies" browserTwoCookies
+      ]
+    , testGroup "Redirection"
+      [ -- See http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
+        -- 300 Multiple Choices: client has to handle this
+        testCase "300" (browserReturnsRedirect 300)
+        -- 301 Moved Permanently: should follow
+      , testCase "301" (browserFollowsRedirect 301)
+        -- 302 Found: should follow
+      , testCase "302" (browserFollowsRedirect 302)
+        -- 303 See Other: should follow (directly for GETs)
+      , testCase "303" (browserFollowsRedirect 303)
+        -- 304 Not Modified: maybe Browser could do something intelligent based on
+        -- being given locally cached content and sending If-Modified-Since, but it
+        -- doesn't at the moment
+      , testCase "304" (browserReturnsRedirect 304)
+      -- 305 Use Proxy: test harness doesn't have a proxy (yet)
+      -- 306 Switch Proxy: obsolete
+      -- 307 Temporary Redirect: should follow
+      , testCase "307" (browserFollowsRedirect 307)
+      -- 308 Resume Incomplete: no support for Resumable HTTP so client has to handle this
+      , testCase "308" (browserReturnsRedirect 308)
+      ]
+    , testGroup "Authentication"
+      [ testCase "Basic" browserBasicAuth
+      , testCase "Digest" browserDigestAuth
+      ]
+    ]
+
+port80Tests =
+    testGroup "Multiple servers"
+    [ testCase "Alternate server" browserAlt
+    , testCase "Both servers" browserBoth
+    , testCase "Both servers (reversed)" browserBothReversed
+    -- github issue 14
+    -- , testCase "Two requests - alternate server" browserTwoRequestsAlt
+    -- , testCase "Two requests - both servers" browserTwoRequestsBoth
+    ]
+
+urlRoot :: Int -> String
+urlRoot 80 = "http://localhost"
+urlRoot n = "http://localhost:" ++ show n
+
+secureRoot :: Int -> String
+secureRoot 443 = "https://localhost"
+secureRoot n = "https://localhost:" ++ show n
+
+type ServerAddress = String -> String
+
+httpAddress, httpsAddress :: Int -> ServerAddress
+httpAddress port p = urlRoot port ++ p
+httpsAddress port p = secureRoot port ++ p
+
+main :: IO ()
+main = do
+  args <- getArgs
+
+  let servers = [("httpd-shed", Httpd.shed), ("warp", Httpd.warp)]
+      basePortNum, altPortNum :: Int
+      basePortNum = 5812
+      altPortNum = 80
+      numberedServers = zip [basePortNum..] servers
+
+  let setupNormalTests = do
+      flip mapM numberedServers $ \(portNum, (serverName, server)) -> do
+         let ?testUrl = httpAddress portNum
+             ?secureTestUrl = httpsAddress portNum
+         _ <- forkIO $ server portNum processRequest
+         return $ testGroup serverName [basicTests, browserTests]
+
+  let setupAltTests = do
+      let (portNum, (_, server)) = head numberedServers
+      let ?testUrl = httpAddress portNum
+          ?altTestUrl = httpAddress altPortNum
+      _ <- forkIO $ server altPortNum altProcessRequest
+      return port80Tests
+
+  case args of
+     ["server"] -> do -- run only the harness servers for diagnostic/debug purposes
+                      -- halt on any keypress
+        _ <- setupNormalTests
+        _ <- setupAltTests
+        _ <- getChar
+        return ()
+     ("--withport80":args) -> do
+        normalTests <- setupNormalTests
+        altTests <- setupAltTests
+        _ <- threadDelay 1000000 -- Give the server time to start :-(
+        defaultMainWithArgs (normalTests ++ [altTests]) args
+     args -> do -- run the test harness as normal
+        normalTests <- setupNormalTests
+        _ <- threadDelay 1000000 -- Give the server time to start :-(
+        defaultMainWithArgs normalTests args
