diff --git a/Network/HTTP/Client.hs b/Network/HTTP/Client.hs
--- a/Network/HTTP/Client.hs
+++ b/Network/HTTP/Client.hs
@@ -43,7 +43,7 @@
 -- A note on exceptions: for the most part, all actions that perform I/O should
 -- be assumed to throw an @HttpException@ in the event of some problem, and all
 -- pure functions will be total. For example, @withResponse@, @httpLbs@, and
--- @brRead@ can all throw exceptions. Functions like @responseStatus@ and
+-- @BodyReader@ can all throw exceptions. Functions like @responseStatus@ and
 -- @applyBasicAuth@ are guaranteed to be total (or there\'s a bug in the
 -- library).
 --
diff --git a/Network/HTTP/Client/Body.hs b/Network/HTTP/Client/Body.hs
--- a/Network/HTTP/Client/Body.hs
+++ b/Network/HTTP/Client/Body.hs
@@ -9,6 +9,7 @@
     , brEmpty
     , brAddCleanup
     , brReadSome
+    , brRead
     ) where
 
 import Network.HTTP.Client.Connection
@@ -21,8 +22,15 @@
 import Control.Monad (unless, when)
 import qualified Data.Streaming.Zlib as Z
 
+-- ^ Get a single chunk of data from the response body, or an empty
+-- bytestring if no more data is available.
+--
+-- Since 0.1.0
+brRead :: BodyReader -> IO S.ByteString
+brRead = id
+
 brReadSome :: BodyReader -> Int -> IO L.ByteString
-brReadSome BodyReader {..} =
+brReadSome brRead =
     loop id
   where
     loop front rem
@@ -34,35 +42,29 @@
                 else loop (front . (bs:)) (rem - S.length bs)
 
 brEmpty :: BodyReader
-brEmpty = BodyReader
-    { brRead = return S.empty
-    , brComplete = return True
-    }
+brEmpty = return S.empty
 
 brAddCleanup :: IO () -> BodyReader -> BodyReader
-brAddCleanup cleanup br = BodyReader
-    { brRead = do
-        bs <- brRead br
-        when (S.null bs) cleanup
-        return bs
-    , brComplete = brComplete br
-    }
+brAddCleanup cleanup brRead = do
+    bs <- brRead
+    when (S.null bs) cleanup
+    return bs
 
 -- | Strictly consume all remaining chunks of data from the stream.
 --
 -- Since 0.1.0
 brConsume :: BodyReader -> IO [S.ByteString]
-brConsume f =
+brConsume brRead =
     go id
   where
     go front = do
-        x <- brRead f
+        x <- brRead
         if S.null x
             then return $ front []
             else go (front . (x:))
 
 makeGzipReader :: BodyReader -> IO BodyReader
-makeGzipReader br = do
+makeGzipReader brRead = do
     inf <- Z.initInflate $ Z.WindowBits 31
     istate <- newIORef Nothing
     let goPopper popper = do
@@ -80,69 +82,55 @@
                             return bs
                 Z.PRError e -> throwIO $ HttpZlibException e
         start = do
-            bs <- brRead br
+            bs <- brRead
             if S.null bs
                 then return S.empty
                 else do
                     popper <- Z.feedInflate inf bs
                     goPopper popper
-    return BodyReader
-        { brRead = do
-            state <- readIORef istate
-            case state of
-                Nothing -> start
-                Just popper -> goPopper popper
-        , brComplete = brComplete br
-        }
+    return $ do
+        state <- readIORef istate
+        case state of
+            Nothing -> start
+            Just popper -> goPopper popper
 
 makeUnlimitedReader :: Connection -> IO BodyReader
 makeUnlimitedReader Connection {..} = do
     icomplete <- newIORef False
-    return $! BodyReader
-        { brRead = do
-            bs <- connectionRead
-            when (S.null bs) $ writeIORef icomplete True
-            return bs
-        , brComplete = readIORef icomplete
-        }
+    return $ do
+        bs <- connectionRead
+        when (S.null bs) $ writeIORef icomplete True
+        return bs
 
 makeLengthReader :: Int -> Connection -> IO BodyReader
 makeLengthReader count0 Connection {..} = do
     icount <- newIORef count0
-    return $! BodyReader
-        { brRead = do
-            count <- readIORef icount
-            if count <= 0
-                then return empty
-                else do
-                    bs <- connectionRead
-                    when (S.null bs) $ throwIO $ ResponseBodyTooShort (fromIntegral count0) (fromIntegral $ count0 - count)
-                    case compare count $ S.length bs of
-                        LT -> do
-                            let (x, y) = S.splitAt count bs
-                            connectionUnread y
-                            writeIORef icount (-1)
-                            return x
-                        EQ -> do
-                            writeIORef icount (-1)
-                            return bs
-                        GT -> do
-                            writeIORef icount (count - S.length bs)
-                            return bs
-        , brComplete = fmap (== -1) $ readIORef icount
-        }
+    return $ do
+        count <- readIORef icount
+        if count <= 0
+            then return empty
+            else do
+                bs <- connectionRead
+                when (S.null bs) $ throwIO $ ResponseBodyTooShort (fromIntegral count0) (fromIntegral $ count0 - count)
+                case compare count $ S.length bs of
+                    LT -> do
+                        let (x, y) = S.splitAt count bs
+                        connectionUnread y
+                        writeIORef icount (-1)
+                        return x
+                    EQ -> do
+                        writeIORef icount (-1)
+                        return bs
+                    GT -> do
+                        writeIORef icount (count - S.length bs)
+                        return bs
 
 makeChunkedReader :: Bool -- ^ raw
                   -> Connection
                   -> IO BodyReader
 makeChunkedReader raw conn@Connection {..} = do
     icount <- newIORef 0
-    return $! BodyReader
-        { brRead = go icount
-        , brComplete = do
-            count <- readIORef icount
-            return $! count == -1
-        }
+    return $ go icount
   where
     go icount = do
         count0 <- readIORef icount
diff --git a/Network/HTTP/Client/Response.hs b/Network/HTTP/Client/Response.hs
--- a/Network/HTTP/Client/Response.hs
+++ b/Network/HTTP/Client/Response.hs
@@ -8,17 +8,13 @@
     , lbsResponse
     ) where
 
-import Control.Arrow (first)
-import Control.Monad (liftM, (>=>))
+import Control.Monad ((>=>))
 
 import Control.Exception (throwIO)
 
-import qualified Data.ByteString as S
 import qualified Data.ByteString.Char8 as S8
 import qualified Data.ByteString.Lazy as L
 
-import qualified Data.CaseInsensitive as CI
-
 import Data.Default.Class (def)
 
 import qualified Network.HTTP.Types as W
@@ -26,12 +22,10 @@
 
 import Network.HTTP.Client.Types
 
-import Network.HTTP.Client.Manager
 import Network.HTTP.Client.Request
 import Network.HTTP.Client.Util
 import Network.HTTP.Client.Body
 import Network.HTTP.Client.Headers
-import Network.HTTP.Client.Connection
 
 import System.Timeout (timeout)
 
@@ -96,7 +90,7 @@
     let timeout' =
             case timeout'' of
                 Nothing -> id
-                Just t -> timeout t >=> maybe responseTimeout return
+                Just t -> timeout t >=> maybe (throwIO ResponseTimeout) return
     StatusHeaders s version hs <- timeout' $ parseStatusHeaders conn
     let mcl = lookup "content-length" hs >>= readDec . S8.unpack
 
@@ -132,5 +126,3 @@
         , responseCookieJar = def
         , responseClose' = ResponseClose (cleanup False)
         }
-  where
-    responseTimeout = throwIO ResponseTimeout
diff --git a/Network/HTTP/Client/Types.hs b/Network/HTTP/Client/Types.hs
--- a/Network/HTTP/Client/Types.hs
+++ b/Network/HTTP/Client/Types.hs
@@ -4,7 +4,7 @@
 {-# LANGUAGE DeriveTraversable #-}
 {-# LANGUAGE RankNTypes #-}
 module Network.HTTP.Client.Types
-    ( BodyReader (..)
+    ( BodyReader
     , Connection (..)
     , StatusHeaders (..)
     , HttpException (..)
@@ -50,19 +50,15 @@
 import Data.Text (Text)
 import Data.Streaming.Zlib (ZlibException)
 
--- | An abstraction for representing an incoming response body coming from the
--- server. Data provided by this abstraction has already been gunzipped and
+-- | An @IO@ action that represents an incoming response body coming from the
+-- server. Data provided by this action has already been gunzipped and
 -- de-chunked, and respects any content-length headers present.
 --
--- Since 0.1.0
-data BodyReader = BodyReader
-    { brRead :: !(IO S.ByteString)
-    -- ^ Get a single chunk of data from the response body, or an empty
-    -- bytestring if no more data is available.
-    --
-    -- Since 0.1.0
-    , brComplete :: !(IO Bool)
-    }
+-- The action gets a single chunk of data from the response body, or an empty
+-- bytestring if no more data is available.
+--
+-- Since 0.4.0
+type BodyReader = IO S.ByteString
 
 data Connection = Connection
     { connectionRead :: !(IO S.ByteString)
diff --git a/http-client.cabal b/http-client.cabal
--- a/http-client.cabal
+++ b/http-client.cabal
@@ -1,5 +1,5 @@
 name:                http-client
-version:             0.3.2.1
+version:             0.3.2.2
 synopsis:            An HTTP client engine, intended as a base layer for more user-friendly packages.
 description:         This codebase has been refactored from http-conduit.
 homepage:            https://github.com/snoyberg/http-client
diff --git a/test/Network/HTTP/Client/BodySpec.hs b/test/Network/HTTP/Client/BodySpec.hs
--- a/test/Network/HTTP/Client/BodySpec.hs
+++ b/test/Network/HTTP/Client/BodySpec.hs
@@ -11,6 +11,11 @@
 main :: IO ()
 main = hspec spec
 
+brComplete :: BodyReader -> IO Bool
+brComplete brRead = do
+  xs <- brRead
+  return (xs == "")
+
 spec :: Spec
 spec = describe "BodySpec" $ do
     it "chunked, single" $ do
@@ -18,89 +23,72 @@
             [ "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"
             ]
         reader <- makeChunkedReader False conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "hello world"
         input' <- input
         S.concat input' `shouldBe` "not consumed"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
+
     it "chunked, pieces" $ do
         (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack
             "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"
         reader <- makeChunkedReader False conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "hello world"
         input' <- input
         S.concat input' `shouldBe` "not consumed"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
+
     it "chunked, raw" $ do
         (conn, _, input) <- dummyConnection
             [ "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"
             ]
         reader <- makeChunkedReader True conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n"
         input' <- input
         S.concat input' `shouldBe` "not consumed"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
 
     it "chunked, pieces, raw" $ do
         (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack
             "5\r\nhello\r\n6\r\n world\r\n0\r\nnot consumed"
         reader <- makeChunkedReader True conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "5\r\nhello\r\n6\r\n world\r\n0\r\n"
         input' <- input
         S.concat input' `shouldBe` "not consumed"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
 
     it "length, single" $ do
         (conn, _, input) <- dummyConnection
             [ "hello world done"
             ]
         reader <- makeLengthReader 11 conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "hello world"
         input' <- input
         S.concat input' `shouldBe` " done"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
+
     it "length, pieces" $ do
         (conn, _, input) <- dummyConnection $ map S.singleton $ S.unpack
             "hello world done"
         reader <- makeLengthReader 11 conn
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         S.concat body `shouldBe` "hello world"
         input' <- input
         S.concat input' `shouldBe` " done"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
+
     it "gzip" $ do
         let orig = L.fromChunks $ replicate 5000 "Hello world!"
             origZ = compress orig
         (conn, _, input) <- dummyConnection $ L.toChunks origZ ++ ["ignored"]
         reader' <- makeLengthReader (fromIntegral $ L.length origZ) conn
         reader <- makeGzipReader reader'
-        complete1 <- brComplete reader
-        complete1 `shouldBe` False
         body <- brConsume reader
         L.fromChunks body `shouldBe` orig
         input' <- input
         S.concat input' `shouldBe` "ignored"
-        complete2 <- brComplete reader
-        complete2 `shouldBe` True
+        brComplete reader `shouldReturn` True
diff --git a/test/Network/HTTP/ClientSpec.hs b/test/Network/HTTP/ClientSpec.hs
--- a/test/Network/HTTP/ClientSpec.hs
+++ b/test/Network/HTTP/ClientSpec.hs
@@ -13,6 +13,7 @@
 import           Test.Hspec
 import qualified Data.Streaming.Network    as N
 import qualified Data.ByteString           as S
+import           Data.ByteString.Lazy.Char8 () -- orphan instance
 
 main :: IO ()
 main = hspec spec
