diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,8 @@
+## 3.2.11.2
+
+* Don't throw exceptions when closing a keep-alive connection
+  [#618](https://github.com/yesodweb/wai/issues/618)
+
 ## 3.2.11.1
 
 * Move exception handling to top of thread (fixes
diff --git a/Network/Wai/Handler/Warp/Request.hs b/Network/Wai/Handler/Warp/Request.hs
--- a/Network/Wai/Handler/Warp/Request.hs
+++ b/Network/Wai/Handler/Warp/Request.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
 {-# OPTIONS_GHC -fno-warn-deprecations #-}
 
 module Network.Wai.Handler.Warp.Request (
@@ -8,12 +9,14 @@
   , headerLines
   , pauseTimeoutKey
   , getFileInfoKey
+  , NoKeepAliveRequest (..)
   ) where
 
 import qualified Control.Concurrent as Conc (yield)
-import Control.Exception (throwIO)
+import Control.Exception (throwIO, Exception)
 import Data.Array ((!))
 import Data.ByteString (ByteString)
+import Data.Typeable (Typeable)
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Unsafe as SU
 import qualified Data.CaseInsensitive as CI
@@ -46,7 +49,8 @@
 
 -- | Receiving a HTTP request from 'Connection' and parsing its header
 --   to create 'Request'.
-recvRequest :: Settings
+recvRequest :: Bool -- ^ first request on this connection?
+            -> Settings
             -> Connection
             -> InternalInfo1
             -> SockAddr -- ^ Peer's address.
@@ -61,8 +65,8 @@
             -- 'IndexedHeader' of HTTP request for internal use,
             -- Body producing action used for flushing the request body
 
-recvRequest settings conn ii1 addr src = do
-    hdrlines <- headerLines src
+recvRequest firstRequest settings conn ii1 addr src = do
+    hdrlines <- headerLines firstRequest src
     (method, unparsedPath, path, query, httpversion, hdr) <- parseHeaderLines hdrlines
     let idxhdr = indexRequestHeader hdr
         expect = idxhdr ! fromEnum ReqExpect
@@ -103,12 +107,20 @@
 
 ----------------------------------------------------------------
 
-headerLines :: Source -> IO [ByteString]
-headerLines src = do
+headerLines :: Bool -> Source -> IO [ByteString]
+headerLines firstRequest src = do
     bs <- readSource src
     if S.null bs
-        then throwIO ConnectionClosedByPeer
+        -- When we're working on a keep-alive connection and trying to
+        -- get the second or later request, we don't want to treat the
+        -- lack of data as a real exception. See the http1 function in
+        -- the Run module for more details.
+        then if firstRequest then throwIO ConnectionClosedByPeer else throwIO NoKeepAliveRequest
         else push src (THStatus 0 id id) bs
+
+data NoKeepAliveRequest = NoKeepAliveRequest
+    deriving (Show, Typeable)
+instance Exception NoKeepAliveRequest
 
 ----------------------------------------------------------------
 
diff --git a/Network/Wai/Handler/Warp/Run.hs b/Network/Wai/Handler/Warp/Run.hs
--- a/Network/Wai/Handler/Warp/Run.hs
+++ b/Network/Wai/Handler/Warp/Run.hs
@@ -341,7 +341,7 @@
         writeIORef istatus True
         leftoverSource src bs
         addr <- getProxyProtocolAddr src
-        http1 addr istatus src `E.catch` \e -> do
+        http1 True addr istatus src `E.catch` \e -> do
             sendErrorResponse addr istatus e
             throwIO (e :: SomeException)
   where
@@ -402,8 +402,8 @@
 
     errorResponse e = settingsOnExceptionResponse settings e
 
-    http1 addr istatus src = do
-        (req', mremainingRef, idxhdr, nextBodyFlush, ii) <- recvRequest settings conn ii1 addr src
+    http1 firstRequest addr istatus src = do
+        (req', mremainingRef, idxhdr, nextBodyFlush, ii) <- recvRequest firstRequest settings conn ii1 addr src
         let req = req' { isSecure = isTransportSecure transport }
         keepAlive <- processRequest istatus src req mremainingRef idxhdr nextBodyFlush ii
             `E.catch` \e -> do
@@ -412,7 +412,16 @@
                 settingsOnException settings (Just req) e
                 -- Don't throw the error again to prevent calling settingsOnException twice.
                 return False
-        when keepAlive $ http1 addr istatus src
+
+        -- When doing a keep-alive connection, the other side may just
+        -- close the connection. We don't want to treat that as an
+        -- exceptional situation, so we pass in False to http1 (which
+        -- in turn passes in False to recvRequest), indicating that
+        -- this is not the first request. If, when trying to read the
+        -- request headers, no data is available, recvRequest will
+        -- throw a NoKeepAliveRequest exception, which we catch here
+        -- and ignore. See: https://github.com/yesodweb/wai/issues/618
+        when keepAlive $ http1 False addr istatus src `E.catch` \NoKeepAliveRequest -> return ()
 
     processRequest istatus src req mremainingRef idxhdr nextBodyFlush ii = do
         -- Let the application run for as long as it wants
diff --git a/test/RequestSpec.hs b/test/RequestSpec.hs
--- a/test/RequestSpec.hs
+++ b/test/RequestSpec.hs
@@ -63,29 +63,29 @@
   describe "headerLines" $ do
       it "can handle a nomarl case" $ do
           src <- mkSourceFunc ["Status: 200\r\nContent-Type: text/plain\r\n\r\n"] >>= mkSource
-          x <- headerLines src
+          x <- headerLines True src
           x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
 
       it "can handle a nasty case (1)" $ do
           src <- mkSourceFunc ["Status: 200", "\r\nContent-Type: text/plain", "\r\n\r\n"] >>= mkSource
-          x <- headerLines src
+          x <- headerLines True src
           x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
 
       it "can handle a nasty case (1)" $ do
           src <- mkSourceFunc ["Status: 200", "\r", "\nContent-Type: text/plain", "\r", "\n\r\n"] >>= mkSource
-          x <- headerLines src
+          x <- headerLines True src
           x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
 
       it "can handle a nasty case (1)" $ do
           src <- mkSourceFunc ["Status: 200", "\r", "\n", "Content-Type: text/plain", "\r", "\n", "\r", "\n"] >>= mkSource
-          x <- headerLines src
+          x <- headerLines True src
           x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
 
       it "can handle an illegal case (1)" $ do
           src <- mkSourceFunc ["\nStatus:", "\n 200", "\nContent-Type: text/plain", "\r\n\r\n"] >>= mkSource
-          x <- headerLines src
+          x <- headerLines True src
           x `shouldBe` []
-          y <- headerLines src
+          y <- headerLines True src
           y `shouldBe` ["Status: 200", "Content-Type: text/plain"]
 
   where
@@ -111,7 +111,7 @@
                     writeIORef ref z
                     return y
     src' <- mkSource src
-    res <- headerLines src'
+    res <- headerLines True src'
     return (res, src')
 
 consumeLen :: Int -> Source -> IO S8.ByteString
diff --git a/warp.cabal b/warp.cabal
--- a/warp.cabal
+++ b/warp.cabal
@@ -1,5 +1,5 @@
 Name:                warp
-Version:             3.2.11.1
+Version:             3.2.11.2
 Synopsis:            A fast, light-weight web server for WAI applications.
 License:             MIT
 License-file:        LICENSE
