diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,10 @@
+## 3.1.10
+
+* `setFileInfoCacheDuration`
+* `setLogger`
+* `FileInfo`/`getFileInfo`
+* Fix: warp-tls strips out the Host request header [#478](https://github.com/yesodweb/wai/issues/478)
+
 ## 3.1.9
 
 * Using the new priority queue based on PSQ provided by http2 lib again.
diff --git a/Network/Wai/Handler/Warp.hs b/Network/Wai/Handler/Warp.hs
--- a/Network/Wai/Handler/Warp.hs
+++ b/Network/Wai/Handler/Warp.hs
@@ -66,6 +66,7 @@
   , setTimeout
   , setManager
   , setFdCacheDuration
+  , setFileInfoCacheDuration
   , setBeforeMainLoop
   , setNoParsePath
   , setInstallShutdownHandler
@@ -77,6 +78,7 @@
   , setProxyProtocolOptional
   , setSlowlorisSize
   , setHTTP2Disabled
+  , setLogger
     -- ** Getters
   , getPort
   , getHost
@@ -95,6 +97,8 @@
   , InvalidRequest (..)
     -- * Utilities
   , pauseTimeout
+  , FileInfo(..)
+  , getFileInfo
     -- * Internal
     -- | The following APIs will be removed in Warp 3.2.0. Please use ones exported from Network.Wai.Handler.Warp.Internal.
 
@@ -136,16 +140,18 @@
   , module Network.Wai.Handler.Warp.Timeout
   ) where
 
-import Control.Exception (SomeException)
+import Control.Exception (SomeException, throwIO)
 import Data.ByteString (ByteString)
 import Data.Maybe (fromMaybe)
 import Data.Streaming.Network (HostPreference)
 import qualified Data.Vault.Lazy as Vault
+import qualified Network.HTTP.Types as H
 import Network.Socket (SockAddr)
 import Network.Wai (Request, Response, vault)
 import Network.Wai.Handler.Warp.Buffer
 import Network.Wai.Handler.Warp.Date
 import Network.Wai.Handler.Warp.FdCache
+import Network.Wai.Handler.Warp.FileInfoCache
 import Network.Wai.Handler.Warp.Header
 import Network.Wai.Handler.Warp.Request
 import Network.Wai.Handler.Warp.Response
@@ -219,10 +225,26 @@
 -- Enabling this cache results in drastic performance improvement for file
 -- transfers.
 --
--- Default value: since 3.0.13, default value is 0, was previously 10
+-- Default value: 0, was previously 10
+--
+-- Since 3.0.13
 setFdCacheDuration :: Int -> Settings -> Settings
 setFdCacheDuration x y = y { settingsFdCacheDuration = x }
 
+-- | Cache duration time of file information in seconds. 0 means that the cache mechanism is not used.
+--
+-- The file information cache is an optimization that is useful for servers dealing with
+-- static files. However, if files are being modified, it can cause incorrect
+-- results in some cases. Therefore, we disable it by default. If you know that
+-- your files will be static or you prefer performance to file consistency,
+-- it's recommended to turn this on; a reasonable value for those cases is 10.
+-- Enabling this cache results in drastic performance improvement for file
+-- transfers.
+--
+-- Default value: 0
+setFileInfoCacheDuration :: Int -> Settings -> Settings
+setFileInfoCacheDuration x y = y { settingsFileInfoCacheDuration = x }
+
 -- | Code to run after the listening socket is ready but before entering
 -- the main event loop. Useful for signaling to tests that they can start
 -- running, or to drop permissions after binding to a restricted port.
@@ -363,6 +385,13 @@
 setHTTP2Disabled :: Settings -> Settings
 setHTTP2Disabled y = y { settingsHTTP2Enabled = False }
 
+-- | Setting a log function.
+--
+-- Since 3.X.X
+setLogger :: (Request -> H.Status -> Maybe Integer -> IO ())
+          -> Settings -> Settings
+setLogger lgr y = y { settingsLogger = lgr }
+
 -- | Explicitly pause the slowloris timeout.
 --
 -- This is useful for cases where you partially consume a request body. For
@@ -371,3 +400,22 @@
 -- Since 3.0.10
 pauseTimeout :: Request -> IO ()
 pauseTimeout = fromMaybe (return ()) . Vault.lookup pauseTimeoutKey . vault
+
+-- | Getting file information of the target file.
+--
+--   This function first uses a stat(2) or similar system call
+--   to obtain information of the target file, then registers
+--   it into the internal cache.
+--   From the next time, the information is obtained
+--   from the cache. This reduces the overhead to call the system call.
+--   The internal cache is refreshed every duration specified by
+--   'setFileInfoCacheDuration'.
+--
+--   This function throws an 'IO' exception if the information is not
+--   available. For instance, the target file does not exist.
+--   If this function is used an a Request generated by a WAI
+--   backend besides Warp, it also throws an 'IO' exception.
+--
+-- Since 3.1.10
+getFileInfo :: Request -> FilePath -> IO FileInfo
+getFileInfo = fromMaybe (\_ -> throwIO (userError "getFileInfo")) . Vault.lookup getFileInfoKey . vault
diff --git a/Network/Wai/Handler/Warp/Date.hs b/Network/Wai/Handler/Warp/Date.hs
--- a/Network/Wai/Handler/Warp/Date.hs
+++ b/Network/Wai/Handler/Warp/Date.hs
@@ -5,6 +5,9 @@
   , getDate
   , DateCache
   , GMTDate
+#if WINDOWS
+  , uToH
+#endif
   ) where
 
 #if __GLASGOW_HASKELL__ < 709
@@ -12,16 +15,13 @@
 #endif
 import Control.AutoUpdate (defaultUpdateSettings, updateAction, mkAutoUpdate)
 import Data.ByteString.Char8
+import Network.HTTP.Date
 
 #if WINDOWS
-import Data.Time (formatTime, getCurrentTime)
-# if MIN_VERSION_time(1,5,0)
-import Data.Time (defaultTimeLocale)
-# else
-import System.Locale (defaultTimeLocale)
-# endif
+import Data.Time (UTCTime, formatTime, getCurrentTime)
+import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
+import Foreign.C.Types (CTime(..))
 #else
-import Network.HTTP.Date
 import System.Posix (epochTime)
 #endif
 
@@ -36,17 +36,21 @@
 withDateCache action = initialize >>= action
 
 initialize :: IO DateCache
-initialize = mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentGMTDate }
+initialize = mkAutoUpdate defaultUpdateSettings {
+                            updateAction = formatHTTPDate <$> getCurrentHTTPDate
+                          }
 
--- | Getting 'GMTDate' based on 'DateCache'.
+-- | Getting current 'GMTDate' based on 'DateCache'.
 getDate :: DateCache -> IO GMTDate
 getDate = id
 
-getCurrentGMTDate :: IO GMTDate
 #ifdef WINDOWS
-getCurrentGMTDate = formatDate <$> getCurrentTime
-  where
-    formatDate = pack . formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT"
+uToH :: UTCTime -> HTTPDate
+uToH = epochTimeToHTTPDate . CTime . truncate . utcTimeToPOSIXSeconds
+
+getCurrentHTTPDate :: IO HTTPDate
+getCurrentHTTPDate =  uToH <$> getCurrentTime
 #else
-getCurrentGMTDate = formatHTTPDate . epochTimeToHTTPDate <$> epochTime
+getCurrentHTTPDate :: IO HTTPDate
+getCurrentHTTPDate = epochTimeToHTTPDate <$> epochTime
 #endif
diff --git a/Network/Wai/Handler/Warp/File.hs b/Network/Wai/Handler/Warp/File.hs
new file mode 100644
--- /dev/null
+++ b/Network/Wai/Handler/Warp/File.hs
@@ -0,0 +1,187 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+
+module Network.Wai.Handler.Warp.File (
+    RspFileInfo(..)
+  , conditionalRequest
+  , addContentHeadersForFilePart
+  ) where
+
+import Control.Applicative ((<|>))
+import Data.Array ((!))
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B hiding (pack)
+import qualified Data.ByteString.Builder as B
+import qualified Data.ByteString.Char8 as B (pack, readInteger)
+import qualified Data.ByteString.Lazy as L
+import Network.HTTP.Date
+import qualified Network.HTTP.Types as H
+import qualified Network.HTTP.Types.Header as H
+import Network.Wai
+import qualified Network.Wai.Handler.Warp.FileInfoCache as I
+import Network.Wai.Handler.Warp.Header
+import Numeric (showInt)
+
+----------------------------------------------------------------
+
+data RspFileInfo = WithoutBody H.Status
+                 | WithBody H.Status H.ResponseHeaders Integer Integer
+                 deriving (Eq,Show)
+
+----------------------------------------------------------------
+
+conditionalRequest :: I.FileInfo
+                   -> H.ResponseHeaders -> IndexedHeader
+                   -> RspFileInfo
+conditionalRequest finfo hs0 reqidx = case condition of
+    nobody@(WithoutBody _) -> nobody
+    WithBody s _ off len   -> let hs = (H.hLastModified,date) :
+                                       addContentHeaders hs0 off len size
+                              in WithBody s hs off len
+  where
+    mtime = I.fileInfoTime finfo
+    size  = I.fileInfoSize finfo
+    date  = I.fileInfoDate finfo
+    mcondition = ifmodified    reqidx size mtime
+             <|> ifunmodified  reqidx size mtime
+             <|> ifrange       reqidx size mtime
+    condition = case mcondition of
+        Nothing -> unconditional reqidx size
+        Just x  -> x
+
+----------------------------------------------------------------
+
+ifModifiedSince :: IndexedHeader -> Maybe HTTPDate
+ifModifiedSince reqidx = reqidx ! fromEnum ReqIfModifiedSince >>= parseHTTPDate
+
+ifUnmodifiedSince :: IndexedHeader -> Maybe HTTPDate
+ifUnmodifiedSince reqidx = reqidx ! fromEnum ReqIfUnmodifiedSince >>= parseHTTPDate
+
+ifRange :: IndexedHeader -> Maybe HTTPDate
+ifRange reqidx = reqidx ! fromEnum ReqIfRange >>= parseHTTPDate
+
+----------------------------------------------------------------
+
+ifmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
+ifmodified reqidx size mtime = do
+    date <- ifModifiedSince reqidx
+    return $ if date /= mtime
+             then unconditional reqidx size
+             else  WithoutBody H.notModified304
+
+ifunmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
+ifunmodified reqidx size mtime = do
+    date <- ifUnmodifiedSince reqidx
+    return $ if date == mtime
+             then unconditional reqidx size
+             else WithoutBody H.preconditionFailed412
+
+ifrange :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
+ifrange reqidx size mtime = do
+    date <- ifRange reqidx
+    rng  <- reqidx ! fromEnum ReqRange
+    return $ if date == mtime
+             then parseRange rng size
+             else WithBody H.ok200 [] 0 size
+
+unconditional :: IndexedHeader -> Integer -> RspFileInfo
+unconditional reqidx size = case reqidx ! fromEnum ReqRange of
+    Nothing  -> WithBody H.ok200 [] 0 size
+    Just rng -> parseRange rng size
+
+----------------------------------------------------------------
+
+parseRange :: ByteString -> Integer -> RspFileInfo
+parseRange rng size = case parseByteRanges rng of
+    Nothing    -> WithoutBody H.requestedRangeNotSatisfiable416
+    Just []    -> WithoutBody H.requestedRangeNotSatisfiable416
+    Just (r:_) -> let (!beg, !end) = checkRange r size
+                      !len = end - beg + 1
+                      s = if beg == 0 && end == size - 1 then
+                              H.ok200
+                            else
+                              H.partialContent206
+                  in WithBody s [] beg len
+
+checkRange :: H.ByteRange -> Integer -> (Integer, Integer)
+checkRange (H.ByteRangeFrom   beg)     size = (beg, size - 1)
+checkRange (H.ByteRangeFromTo beg end) size = (beg,  min (size - 1) end)
+checkRange (H.ByteRangeSuffix count)   size = (max 0 (size - count), size - 1)
+
+-- | Parse the value of a Range header into a 'H.ByteRanges'.
+parseByteRanges :: B.ByteString -> Maybe H.ByteRanges
+parseByteRanges bs1 = do
+    bs2 <- stripPrefix "bytes=" bs1
+    (r, bs3) <- range bs2
+    ranges (r:) bs3
+  where
+    range bs2 = do
+        (i, bs3) <- B.readInteger bs2
+        if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-")
+            then Just (H.ByteRangeSuffix (negate i), bs3)
+            else do
+                bs4 <- stripPrefix "-" bs3
+                case B.readInteger bs4 of
+                    Just (j, bs5) | j >= i -> Just (H.ByteRangeFromTo i j, bs5)
+                    _ -> Just (H.ByteRangeFrom i, bs4)
+    ranges front bs3
+        | B.null bs3 = Just (front [])
+        | otherwise = do
+            bs4 <- stripPrefix "," bs3
+            (r, bs5) <- range bs4
+            ranges (front . (r:)) bs5
+
+    stripPrefix x y
+        | x `B.isPrefixOf` y = Just (B.drop (B.length x) y)
+        | otherwise = Nothing
+
+----------------------------------------------------------------
+
+-- | @contentRangeHeader beg end total@ constructs a Content-Range 'H.Header'
+-- for the range specified.
+contentRangeHeader :: Integer -> Integer -> Integer -> H.Header
+contentRangeHeader beg end total = (
+#if MIN_VERSION_http_types(0,9,0)
+        H.hContentRange
+#else
+        "Content-Range"
+#endif
+    , range)
+  where
+    range = B.pack
+      -- building with ShowS
+      $ 'b' : 'y': 't' : 'e' : 's' : ' '
+      : (if beg > end then ('*':) else
+          showInt beg
+          . ('-' :)
+          . showInt end)
+      ( '/'
+      : showInt total "")
+
+addContentHeaders :: H.ResponseHeaders -> Integer -> Integer -> Integer -> H.ResponseHeaders
+addContentHeaders hs off len size = hs''
+  where
+    contentRange = contentRangeHeader off (off + len - 1) size
+    lengthBS = L.toStrict $ B.toLazyByteString $ B.integerDec len
+    hs' = (H.hContentLength, lengthBS):(
+#if MIN_VERSION_http_types(0,9,0)
+        H.hAcceptRanges
+#else
+        "Accept-Ranges"
+#endif
+        , "bytes"):hs
+    hs'' = if len == size then hs' else contentRange:hs'
+
+-- |
+--
+-- >>> addContentHeadersForFilePart [] (FilePart 2 10 16)
+-- [("Content-Range","bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")]
+-- >>> addContentHeadersForFilePart [] (FilePart 0 16 16)
+-- [("Content-Length","16"),("Accept-Ranges","bytes")]
+addContentHeadersForFilePart :: H.ResponseHeaders -> FilePart -> H.ResponseHeaders
+addContentHeadersForFilePart hs part = addContentHeaders hs off len size
+  where
+    off = filePartOffset part
+    len = filePartByteCount part
+    size = filePartFileSize part
diff --git a/Network/Wai/Handler/Warp/FileInfoCache.hs b/Network/Wai/Handler/Warp/FileInfoCache.hs
new file mode 100644
--- /dev/null
+++ b/Network/Wai/Handler/Warp/FileInfoCache.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE RecordWildCards, CPP #-}
+
+module Network.Wai.Handler.Warp.FileInfoCache (
+    FileInfo(..)
+  , withFileInfoCache
+  , getInfo -- test purpose only
+  ) where
+
+import Control.Exception as E
+import Control.Monad (void)
+import Control.Reaper
+import Data.ByteString (ByteString)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as M
+import Network.HTTP.Date
+import System.PosixCompat.Files
+
+#if WINDOWS
+import Data.Time (UTCTime)
+import Network.Wai.Handler.Warp.Date (uToH)
+#endif
+
+----------------------------------------------------------------
+
+-- | File information.
+data FileInfo = FileInfo {
+    fileInfoName :: !FilePath
+  , fileInfoSize :: !Integer
+  , fileInfoTime :: HTTPDate   -- ^ Modification time
+  , fileInfoDate :: ByteString -- ^ Modification time in the GMT format
+  } deriving (Eq, Show)
+
+data Entry = Negative | Positive FileInfo
+type Cache = HashMap FilePath Entry
+type FileInfoCache = Reaper Cache (FilePath,Entry)
+
+----------------------------------------------------------------
+
+-- | Getting the file information corresponding to the file.
+getInfo :: FilePath -> IO FileInfo
+getInfo path = do
+    fs <- getFileStatus path -- file access
+    let regular = not (isDirectory fs)
+        readable = fileMode fs `intersectFileModes` ownerReadMode /= 0
+    if regular && readable then do
+        let time = epochTimeToHTTPDate $ modificationTime fs
+            date = formatHTTPDate time
+            size = fromIntegral $ fileSize fs
+            info = FileInfo {
+                fileInfoName = path
+              , fileInfoSize = size
+              , fileInfoTime = time
+              , fileInfoDate = date
+              }
+        return info
+      else
+        throwIO (userError "FileInfoCache:getInfo")
+
+----------------------------------------------------------------
+
+getAndRegisterInfo :: FileInfoCache -> FilePath -> IO FileInfo
+getAndRegisterInfo reaper@Reaper{..} path = do
+    cache <- reaperRead
+    case M.lookup path cache of
+        Just Negative     -> throwIO (userError "FileInfoCache:getAndRegisterInfo")
+        Just (Positive x) -> return x
+        Nothing           -> positive reaper path `E.onException` negative reaper path
+
+positive :: FileInfoCache -> FilePath -> IO FileInfo
+positive Reaper{..} path = do
+    info <- getInfo path
+    reaperAdd (path, Positive info)
+    return info
+
+negative :: FileInfoCache -> FilePath -> IO FileInfo
+negative Reaper{..} path = do
+    reaperAdd (path,Negative)
+    throwIO (userError "FileInfoCache:negative")
+
+----------------------------------------------------------------
+
+-- | Creating a file information cache
+--   and executing the action in the second argument.
+--   The first argument is a cache duration in second.
+withFileInfoCache :: Int -> ((FilePath -> IO FileInfo) -> IO a) -> IO a
+withFileInfoCache 0        action = action getInfo
+withFileInfoCache duration action = E.bracket (initialize duration)
+                                              terminate
+                                              (\c -> action (getAndRegisterInfo c))
+
+initialize :: Int -> IO FileInfoCache
+initialize duration = mkReaper settings
+  where
+    settings = defaultReaperSettings {
+        reaperAction = override
+      , reaperDelay  = duration * 1000000
+      , reaperCons   = uncurry M.insert
+      , reaperNull   = M.null
+      , reaperEmpty  = M.empty
+      }
+
+override :: Cache -> IO (Cache -> Cache)
+override _ = return $ const M.empty
+
+terminate :: FileInfoCache -> IO ()
+terminate x = void $ reaperStop x
diff --git a/Network/Wai/Handler/Warp/HTTP2/Receiver.hs b/Network/Wai/Handler/Warp/HTTP2/Receiver.hs
--- a/Network/Wai/Handler/Warp/HTTP2/Receiver.hs
+++ b/Network/Wai/Handler/Warp/HTTP2/Receiver.hs
@@ -246,7 +246,7 @@
         let !siz = BS.length frag
         return $ Open $ Continued [frag] siz 1 endOfStream pri
 
-stream FrameHeaders header@FrameHeader{flags} bs _ s@(Open (Body q)) _ = do
+stream FrameHeaders header@FrameHeader{flags} bs _ (Open (Body q)) _ = do
     -- trailer is not supported.
     -- let's read and ignore it.
     HeadersFrame _ _ <- guardIt $ decodeHeadersFrame header bs
@@ -255,7 +255,8 @@
         atomically $ writeTQueue q ""
         return HalfClosed
       else
-        return s
+        -- we don't support continuation here.
+        E.throwIO $ ConnectionError ProtocolError "continuation in trailer is not supported"
 
 stream FrameData
        header@FrameHeader{flags,payloadLength,streamId}
diff --git a/Network/Wai/Handler/Warp/HTTP2/Request.hs b/Network/Wai/Handler/Warp/HTTP2/Request.hs
--- a/Network/Wai/Handler/Warp/HTTP2/Request.hs
+++ b/Network/Wai/Handler/Warp/HTTP2/Request.hs
@@ -55,7 +55,9 @@
       , pathInfo = H.decodePathSegments path
       , rawQueryString = query
       , queryString = H.parseQuery query
-      , requestHeaders = hdr
+      , requestHeaders = case ma of
+                           Nothing -> hdr
+                           Just h  -> (mk "host", h) : hdr
       , isSecure = True
       , remoteHost = addr
       , requestBody = body
@@ -110,7 +112,7 @@
                               else
                                 Nothing
       | k == "content-length"
-                          = normal kvs (p { contentLen = Just v },b)
+                          = normal kvs (p { contentLen = Just v }, b . ((mk k,v) :))
       | k == "host"       = if isJust (colonAuth p) then
                                 normal kvs (p,b)
                               else
diff --git a/Network/Wai/Handler/Warp/HTTP2/Sender.hs b/Network/Wai/Handler/Warp/HTTP2/Sender.hs
--- a/Network/Wai/Handler/Warp/HTTP2/Sender.hs
+++ b/Network/Wai/Handler/Warp/HTTP2/Sender.hs
@@ -310,8 +310,10 @@
             Just (SFile path part) -> do
                 (leftover, len) <- runStreamFile ii buf room path part
                 let !total' = total + len
-                return (leftover, Nothing, total')
-                -- TODO if file part is done, go back to loop
+                case leftover of
+                    LZero -> loop (buf `plusPtr` len) (room - len) total'
+                    _     -> return (leftover, Nothing, total')
+
             Just SFlush  -> return (LZero, Nothing, total)
             Just (SFinish trailers) -> return (LZero, Just trailers, total)
 
diff --git a/Network/Wai/Handler/Warp/Header.hs b/Network/Wai/Handler/Warp/Header.hs
--- a/Network/Wai/Handler/Warp/Header.hs
+++ b/Network/Wai/Handler/Warp/Header.hs
@@ -17,29 +17,35 @@
 indexRequestHeader :: RequestHeaders -> IndexedHeader
 indexRequestHeader hdr = traverseHeader hdr requestMaxIndex requestKeyIndex
 
-idxContentLength,idxTransferEncoding,idxExpect :: Int
-idxConnection,idxRange,idxHost :: Int
-idxContentLength    = 0
-idxTransferEncoding = 1
-idxExpect           = 2
-idxConnection       = 3
-idxRange            = 4
-idxHost             = 5
+data RequestHeaderIndex = ReqContentLength
+                        | ReqTransferEncoding
+                        | ReqExpect
+                        | ReqConnection
+                        | ReqRange
+                        | ReqHost
+                        | ReqIfModifiedSince
+                        | ReqIfUnmodifiedSince
+                        | ReqIfRange
+                        deriving (Enum,Bounded)
 
 -- | The size for 'IndexedHeader' for HTTP Request.
 --   From 0 to this corresponds to \"Content-Length\", \"Transfer-Encoding\",
---   \"Expect\", \"Connection\", \"Range\", and \"Host\".
+--   \"Expect\", \"Connection\", \"Range\", \"Host\",
+--   \"If-Modified-Since\", \"If-Unmodified-Since\" and \"If-Range\".
 requestMaxIndex :: Int
-requestMaxIndex     = 5
+requestMaxIndex = fromEnum (maxBound :: RequestHeaderIndex)
 
 requestKeyIndex :: HeaderName -> Int
-requestKeyIndex "content-length"    = idxContentLength
-requestKeyIndex "transfer-encoding" = idxTransferEncoding
-requestKeyIndex "expect"            = idxExpect
-requestKeyIndex "connection"        = idxConnection
-requestKeyIndex "range"             = idxRange
-requestKeyIndex "host"              = idxHost
-requestKeyIndex _                   = -1
+requestKeyIndex "content-length"      = fromEnum ReqContentLength
+requestKeyIndex "transfer-encoding"   = fromEnum ReqTransferEncoding
+requestKeyIndex "expect"              = fromEnum ReqExpect
+requestKeyIndex "connection"          = fromEnum ReqConnection
+requestKeyIndex "range"               = fromEnum ReqRange
+requestKeyIndex "host"                = fromEnum ReqHost
+requestKeyIndex "if-modified-since"   = fromEnum ReqIfModifiedSince
+requestKeyIndex "if-unmodified-since" = fromEnum ReqIfUnmodifiedSince
+requestKeyIndex "if-range"            = fromEnum ReqIfRange
+requestKeyIndex _                     = -1
 
 defaultIndexRequestHeader :: IndexedHeader
 defaultIndexRequestHeader = array (0,requestMaxIndex) [(i,Nothing)|i<-[0..requestMaxIndex]]
@@ -49,19 +55,19 @@
 indexResponseHeader :: ResponseHeaders -> IndexedHeader
 indexResponseHeader hdr = traverseHeader hdr responseMaxIndex responseKeyIndex
 
-idxServer, idxDate :: Int
---idxContentLength = 0
-idxServer        = 1
-idxDate          = 2
+data ResponseHeaderIndex = ResContentLength
+                         | ResServer
+                         | ResDate
+                         deriving (Enum,Bounded)
 
 -- | The size for 'IndexedHeader' for HTTP Response.
 responseMaxIndex :: Int
-responseMaxIndex = 2
+responseMaxIndex = fromEnum (maxBound :: ResponseHeaderIndex)
 
 responseKeyIndex :: HeaderName -> Int
-responseKeyIndex "content-length" = idxContentLength
-responseKeyIndex "server"         = idxServer
-responseKeyIndex "date"           = idxDate
+responseKeyIndex "content-length" = fromEnum ResContentLength
+responseKeyIndex "server"         = fromEnum ResServer
+responseKeyIndex "date"           = fromEnum ResDate
 responseKeyIndex _                = -1
 
 ----------------------------------------------------------------
diff --git a/Network/Wai/Handler/Warp/Internal.hs b/Network/Wai/Handler/Warp/Internal.hs
--- a/Network/Wai/Handler/Warp/Internal.hs
+++ b/Network/Wai/Handler/Warp/Internal.hs
@@ -67,6 +67,8 @@
   , module Network.Wai.Handler.Warp.Timeout
     -- * File descriptor cache
   , module Network.Wai.Handler.Warp.FdCache
+    -- * File information cache
+  , module Network.Wai.Handler.Warp.FileInfoCache
     -- * Date
   , module Network.Wai.Handler.Warp.Date
     -- * Request and response
@@ -78,6 +80,7 @@
 import Network.Wai.Handler.Warp.Buffer
 import Network.Wai.Handler.Warp.Date
 import Network.Wai.Handler.Warp.FdCache
+import Network.Wai.Handler.Warp.FileInfoCache
 import Network.Wai.Handler.Warp.Header
 import Network.Wai.Handler.Warp.Recv
 import Network.Wai.Handler.Warp.Request
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
@@ -7,6 +7,7 @@
     recvRequest
   , headerLines
   , pauseTimeoutKey
+  , getFileInfoKey
   ) where
 
 import qualified Control.Concurrent as Conc (yield)
@@ -21,6 +22,7 @@
 import Network.Socket (SockAddr)
 import Network.Wai
 import Network.Wai.Handler.Warp.Conduit
+import Network.Wai.Handler.Warp.FileInfoCache
 import Network.Wai.Handler.Warp.Header
 import Network.Wai.Handler.Warp.ReadInt
 import Network.Wai.Handler.Warp.RequestHeader
@@ -61,9 +63,9 @@
     hdrlines <- headerLines src
     (method, unparsedPath, path, query, httpversion, hdr) <- parseHeaderLines hdrlines
     let idxhdr = indexRequestHeader hdr
-        expect = idxhdr ! idxExpect
-        cl = idxhdr ! idxContentLength
-        te = idxhdr ! idxTransferEncoding
+        expect = idxhdr ! fromEnum ReqExpect
+        cl = idxhdr ! fromEnum ReqContentLength
+        te = idxhdr ! fromEnum ReqTransferEncoding
         handle100Continue = handleExpect conn httpversion expect
     (rbody, remainingRef, bodyLength) <- bodyAndSource src cl te
     -- body producing function which will produce '100-continue', if needed
@@ -81,16 +83,17 @@
           , isSecure          = False
           , remoteHost        = addr
           , requestBody       = rbody'
-          , vault             = Vault.insert pauseTimeoutKey
-                                (Timeout.pause th)
-                                Vault.empty
+          , vault             = vaultValue
           , requestBodyLength = bodyLength
-          , requestHeaderHost = idxhdr ! idxHost
-          , requestHeaderRange = idxhdr ! idxRange
+          , requestHeaderHost  = idxhdr ! fromEnum ReqHost
+          , requestHeaderRange = idxhdr ! fromEnum ReqRange
           }
     return (req, remainingRef, idxhdr, rbodyFlush)
   where
     th = threadHandle ii
+    vaultValue = Vault.insert pauseTimeoutKey (Timeout.pause th)
+               $ Vault.insert getFileInfoKey (fileInfo ii)
+                 Vault.empty
 
 ----------------------------------------------------------------
 
@@ -278,3 +281,7 @@
 pauseTimeoutKey :: Vault.Key (IO ())
 pauseTimeoutKey = unsafePerformIO Vault.newKey
 {-# NOINLINE pauseTimeoutKey #-}
+
+getFileInfoKey :: Vault.Key (FilePath -> IO FileInfo)
+getFileInfoKey = unsafePerformIO Vault.newKey
+{-# NOINLINE getFileInfoKey #-}
diff --git a/Network/Wai/Handler/Warp/Response.hs b/Network/Wai/Handler/Warp/Response.hs
--- a/Network/Wai/Handler/Warp/Response.hs
+++ b/Network/Wai/Handler/Warp/Response.hs
@@ -7,7 +7,6 @@
 module Network.Wai.Handler.Warp.Response (
     sendResponse
   , sanitizeHeaderValue -- for testing
-  , fileRange -- for testing
   , warpVersion
   , addDate
   , addServer
@@ -26,7 +25,7 @@
 #if __GLASGOW_HASKELL__ < 709
 import Control.Applicative
 #endif
-import Control.Exception
+import qualified Control.Exception as E
 import Control.Monad (unless, when)
 import Data.Array ((!))
 import Data.ByteString (ByteString)
@@ -57,9 +56,11 @@
 import Network.Wai.Handler.Warp.Buffer (toBuilderBuffer)
 import qualified Network.Wai.Handler.Warp.Date as D
 import qualified Network.Wai.Handler.Warp.FdCache as F
+import Network.Wai.Handler.Warp.File
 import Network.Wai.Handler.Warp.Header
 import Network.Wai.Handler.Warp.IO (toBufIOWith)
 import Network.Wai.Handler.Warp.ResponseHeader
+import Network.Wai.Handler.Warp.Settings
 import qualified Network.Wai.Handler.Warp.Timeout as T
 import Network.Wai.Handler.Warp.Types
 import Network.Wai.Internal
@@ -75,36 +76,14 @@
 
 ----------------------------------------------------------------
 
-fileRange :: H.Status -> H.ResponseHeaders -> FilePath
-          -> Maybe FilePart -> Maybe HeaderValue
-          -> IO (Either IOException
-                        (H.Status, H.ResponseHeaders, Integer, Integer))
-fileRange s0 hs0 path Nothing mRange =
-    fmap (fileRangeSized s0 hs0 Nothing mRange) <$> tryGetFileSize path
-fileRange s0 hs0 _ mPart@(Just part) mRange =
-    return . Right $ fileRangeSized s0 hs0 mPart mRange size
-  where
-    size = filePartFileSize part
-
-fileRangeSized :: H.Status -> H.ResponseHeaders
-               -> Maybe FilePart -> Maybe HeaderValue -> Integer
-               -> (H.Status, H.ResponseHeaders, Integer, Integer)
-fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
-  where
-    part = fromMaybe (chooseFilePart fileSize mRange) mPart
-    beg = filePartOffset part
-    len = filePartByteCount part
-    (s, hs) = adjustForFilePart s0 hs0 $ FilePart beg len fileSize
-
-----------------------------------------------------------------
-
 -- | Sending a HTTP response to 'Connection' according to 'Response'.
 --
---   Applications/middlewares MUST specify a proper 'H.ResponseHeaders'.
+--   Applications/middlewares MUST provide a proper 'H.ResponseHeaders'.
 --   so that inconsistency does not happen.
 --   No header is deleted by this function.
 --
---   Especially, Applications/middlewares MUST take care of
+--   Especially, Applications/middlewares MUST provide a proper
+--   Content-Type. They MUST NOT provide
 --   Content-Length, Content-Range, and Transfer-Encoding
 --   because they are inserted, when necessary,
 --   regardless they already exist.
@@ -116,22 +95,6 @@
 --
 --   There are three basic APIs to create 'Response':
 --
---   ['responseFile' :: 'H.Status' -> 'H.ResponseHeaders' -> 'FilePath' -> 'Maybe' 'FilePart' -> 'Response']
---     HTTP response body is sent by sendfile() for GET method.
---     HTTP response body is not sent by HEAD method.
---     Applications are categorized into simple and sophisticated.
---     Simple applications should specify 'Nothing' to
---     'Maybe' 'FilePart'. The size of the specified file is obtained
---     by disk access. Then Range is handled.
---     Sophisticated applications should specify 'Just' to
---     'Maybe' 'FilePart'. They should treat Range (and If-Range) by
---     themselves. In both cases,
---     Content-Length and Content-Range (if necessary) are automatically
---     added into the HTTP response header.
---     If Content-Length and Content-Range exist in the HTTP response header,
---     they would cause inconsistency.
---     Status is also changed to 206 (Partial Content) if necessary.
---
 --   ['responseBuilder' :: 'H.Status' -> 'H.ResponseHeaders' -> 'Builder' -> 'Response']
 --     HTTP response body is created from 'Builder'.
 --     Transfer-Encoding: chunked is used in HTTP/1.1.
@@ -142,8 +105,29 @@
 --
 --   ['responseRaw' :: ('IO' 'ByteString' -> ('ByteString' -> 'IO' ()) -> 'IO' ()) -> 'Response' -> 'Response']
 --     No header is added and no Transfer-Encoding: is applied.
+--
+--   ['responseFile' :: 'H.Status' -> 'H.ResponseHeaders' -> 'FilePath' -> 'Maybe' 'FilePart' -> 'Response']
+--     HTTP response body is sent (by sendfile(), if possible) for GET method.
+--     HTTP response body is not sent by HEAD method.
+--     Content-Length and Content-Range are automatically
+--     added into the HTTP response header if necessary.
+--     If Content-Length and Content-Range exist in the HTTP response header,
+--     they would cause inconsistency.
+--     \"Accept-Ranges: bytes\" is also inserted.
+--
+--     Applications are categorized into simple and sophisticated.
+--     Sophisticated applications should specify 'Just' to
+--     'Maybe' 'FilePart'. They should treat the conditional request
+--     by themselves. A proper 'Status' (200 or 206) must be provided.
+--
+--     Simple applications should specify 'Nothing' to
+--     'Maybe' 'FilePart'. The size of the specified file is obtained
+--     by disk access or from the file infor cache.
+--     If-Modified-Since, If-Unmodified-Since, If-Range and Range
+--     are processed. Since a proper status is chosen, 'Status' is
+--     ignored. Last-Modified is inserted.
 
-sendResponse :: ByteString -- ^ default server value
+sendResponse :: Settings
              -> Connection
              -> InternalInfo
              -> Request -- ^ HTTP request.
@@ -151,35 +135,48 @@
              -> IO ByteString -- ^ source from client, for raw response
              -> Response -- ^ HTTP response including status code and response header.
              -> IO Bool -- ^ Returing True if the connection is persistent.
-sendResponse defServer conn ii req reqidxhdr src response = do
+sendResponse settings conn ii req reqidxhdr src response = do
     hs <- addServerAndDate hs0
     if hasBody s then do
-        -- HEAD comes here even if it does not have body.
-        sendRsp conn mfdc ver s hs rsp
+        -- The response to HEAD does not have body.
+        -- But to handle the conditional requests defined RFC 7232 and
+        -- to generate appropriate content-length, content-range,
+        -- and status, the response to HEAD is processed here.
+        --
+        -- See definition of rsp below for proper body stripping.
+        (ms, mlen) <- sendRsp conn ii ver s hs rsp
+        case ms of
+            Nothing         -> return ()
+            Just realStatus -> logger req realStatus mlen
         T.tickle th
         return ret
       else do
-        sendResponseNoBody conn ver s hs
+        _ <- sendRsp conn ii ver s hs RspNoBody
+        logger req s Nothing
         T.tickle th
         return isPersist
   where
+    defServer = settingsServerName settings
+    logger = settingsLogger settings
     ver = httpVersion req
     s = responseStatus response
     hs0 = sanitizeHeaders $ responseHeaders response
     rspidxhdr = indexResponseHeader hs0
     th = threadHandle ii
     dc = dateCacher ii
-    mfdc = fdCacher ii
     addServerAndDate = addDate dc rspidxhdr . addServer defServer rspidxhdr
-    mRange = reqidxhdr ! idxRange
     (isPersist,isChunked0) = infoFromRequest req reqidxhdr
     isChunked = not isHead && isChunked0
     (isKeepAlive, needsChunked) = infoFromResponse rspidxhdr (isPersist,isChunked)
     isHead = requestMethod req == H.methodHead
     rsp = case response of
-        ResponseFile _ _ path mPart -> RspFile path mPart mRange isHead (T.tickle th)
-        ResponseBuilder _ _ b       -> RspBuilder b needsChunked
-        ResponseStream _ _ fb       -> RspStream fb needsChunked th
+        ResponseFile _ _ path mPart -> RspFile path mPart reqidxhdr isHead (T.tickle th)
+        ResponseBuilder _ _ b
+          | isHead                  -> RspNoBody
+          | otherwise               -> RspBuilder b needsChunked
+        ResponseStream _ _ fb
+          | isHead                  -> RspNoBody
+          | otherwise               -> RspStream fb needsChunked th
         ResponseRaw raw _           -> RspRaw raw src (T.tickle th)
     ret = case response of
         ResponseFile    {} -> isPersist
@@ -214,7 +211,8 @@
 
 ----------------------------------------------------------------
 
-data Rsp = RspFile FilePath (Maybe FilePart) (Maybe HeaderValue) Bool (IO ())
+data Rsp = RspNoBody
+         | RspFile FilePath (Maybe FilePart) IndexedHeader Bool (IO ())
          | RspBuilder Builder Bool
          | RspStream StreamingBody Bool T.Handle
          | RspRaw (IO ByteString -> (ByteString -> IO ()) -> IO ()) (IO ByteString) (IO ())
@@ -222,50 +220,23 @@
 ----------------------------------------------------------------
 
 sendRsp :: Connection
-        -> Maybe F.MutableFdCache
+        -> InternalInfo
         -> H.HttpVersion
         -> H.Status
         -> H.ResponseHeaders
         -> Rsp
-        -> IO ()
-sendRsp conn mfdc ver s0 hs0 (RspFile path mPart mRange isHead hook) = do
-    ex <- fileRange s0 hs0 path mPart mRange
-    case ex of
-        Left _ex ->
-#ifdef WARP_DEBUG
-          print _ex >>
-#endif
-          sendRsp conn mfdc ver s2 hs2 (RspBuilder body True)
-        Right (s, hs, beg, len)
-          | len >= 0 ->
-            if isHead then
-                sendRsp conn mfdc ver s hs (RspBuilder mempty False)
-              else do
-                lheader <- composeHeader ver s hs
-#ifdef WINDOWS
-                let fid = FileId path Nothing
-                    hook' = hook
-#else
-                (mfd, hook') <- case mfdc of
-                   -- settingsFdCacheDuration is 0
-                   Nothing  -> return (Nothing, hook)
-                   Just fdc -> do
-                      (fd, fresher) <- F.getFd fdc path
-                      return (Just fd, hook >> fresher)
-                let fid = FileId path mfd
-#endif
-                connSendFile conn fid beg len hook' [lheader]
-          | otherwise ->
-            sendRsp conn mfdc ver H.status416
-                (filter (\(k, _) -> k /= "content-length") hs)
-                (RspBuilder mempty True)
-  where
-    s2 = H.status404
-    hs2 =  replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
-    body = byteString "File not found"
+        -> IO (Maybe H.Status, Maybe Integer)
 
 ----------------------------------------------------------------
 
+sendRsp conn _ ver s hs RspNoBody = do
+    -- Not adding Content-Length.
+    -- User agents treats it as Content-Length: 0.
+    composeHeader ver s hs >>= connSendAll conn
+    return (Just s, Nothing)
+
+----------------------------------------------------------------
+
 sendRsp conn _ ver s hs (RspBuilder body needsChunked) = do
     header <- composeHeaderBuilder ver s hs needsChunked
     let hdrBdy
@@ -275,6 +246,7 @@
         buffer = connWriteBuffer conn
         size = connBufferSize conn
     toBufIOWith buffer size (connSendAll conn) hdrBdy
+    return (Just s, Nothing) -- fixme: can we tell the actual sent bytes?
 
 ----------------------------------------------------------------
 
@@ -298,11 +270,13 @@
     when needsChunked $ send chunkedTransferTerminator
     mbs <- finish
     maybe (return ()) (sendFragment conn th) mbs
+    return (Just s, Nothing) -- fixme: can we tell the actual sent bytes?
 
 ----------------------------------------------------------------
 
-sendRsp conn _ _ _ _ (RspRaw withApp src tickle) =
+sendRsp conn _ _ _ _ (RspRaw withApp src tickle) = do
     withApp recv send
+    return (Nothing, Nothing)
   where
     recv = do
         bs <- src
@@ -312,16 +286,77 @@
 
 ----------------------------------------------------------------
 
-sendResponseNoBody :: Connection
-                   -> H.HttpVersion
-                   -> H.Status
-                   -> H.ResponseHeaders
-                   -> IO ()
-sendResponseNoBody conn ver s hs = composeHeader ver s hs >>= connSendAll conn
+-- Sophisticated WAI applications.
+-- We respect s0. s0 MUST be a proper value.
+sendRsp conn ii ver s0 hs0 (RspFile path (Just part) _ isHead hook) =
+    sendRspFile2XX conn ii ver s0 hs path beg len isHead hook
+  where
+    beg = filePartOffset part
+    len = filePartByteCount part
+    hs = addContentHeadersForFilePart hs0 part
 
 ----------------------------------------------------------------
+
+-- Simple WAI applications.
+-- Status is ignored
+sendRsp conn ii ver _ hs0 (RspFile path Nothing idxhdr isHead hook) = do
+    efinfo <- E.try $ fileInfo ii path
+    case efinfo of
+        Left (_ex :: E.IOException) ->
+#ifdef WARP_DEBUG
+          print _ex >>
+#endif
+          sendRspFile404 conn ii ver hs0
+        Right finfo -> case conditionalRequest finfo hs0 idxhdr of
+          WithoutBody s         -> sendRsp conn ii ver s hs0 RspNoBody
+          WithBody s hs beg len -> sendRspFile2XX conn ii ver s hs path beg len isHead hook
+
 ----------------------------------------------------------------
 
+sendRspFile2XX :: Connection
+               -> InternalInfo
+               -> H.HttpVersion
+               -> H.Status
+               -> H.ResponseHeaders
+               -> FilePath
+               -> Integer
+               -> Integer
+               -> Bool
+               -> IO ()
+               -> IO (Maybe H.Status, Maybe Integer)
+sendRspFile2XX conn ii ver s hs path beg len isHead hook
+  | isHead = sendRsp conn ii ver s hs RspNoBody
+  | otherwise = do
+      lheader <- composeHeader ver s hs
+#ifdef WINDOWS
+      let fid = FileId path Nothing
+          hook' = hook
+#else
+      (mfd, hook') <- case fdCacher ii of
+         -- settingsFdCacheDuration is 0
+         Nothing  -> return (Nothing, hook)
+         Just fdc -> do
+            (fd, fresher) <- F.getFd fdc path
+            return (Just fd, hook >> fresher)
+      let fid = FileId path mfd
+#endif
+      connSendFile conn fid beg len hook' [lheader]
+      return (Just s, Just len)
+
+sendRspFile404 :: Connection
+               -> InternalInfo
+               -> H.HttpVersion
+               -> H.ResponseHeaders
+               -> IO (Maybe H.Status, Maybe Integer)
+sendRspFile404 conn ii ver hs0 = sendRsp conn ii ver s hs (RspBuilder body True)
+  where
+    s = H.notFound404
+    hs =  replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
+    body = byteString "File not found"
+
+----------------------------------------------------------------
+----------------------------------------------------------------
+
 -- | Use 'connSendAll' to send this data while respecting timeout rules.
 sendFragment :: Connection -> T.Handle -> ByteString -> IO ()
 sendFragment Connection { connSendAll = send } th bs = do
@@ -345,7 +380,7 @@
     | otherwise       = checkPersist10 conn
   where
     ver = httpVersion req
-    conn = reqidxhdr ! idxConnection
+    conn = reqidxhdr ! fromEnum ReqConnection
     checkPersist11 (Just x)
         | CI.foldCase x == "close"      = False
     checkPersist11 _                    = True
@@ -370,7 +405,7 @@
   where
     needsChunked = isChunked && not hasLength
     isKeepAlive = isPersist && (isChunked || hasLength)
-    hasLength = isJust $ rspidxhdr ! idxContentLength
+    hasLength = isJust $ rspidxhdr ! fromEnum ResContentLength
 
 ----------------------------------------------------------------
 
@@ -391,7 +426,7 @@
 #endif
 
 addDate :: D.DateCache -> IndexedHeader -> H.ResponseHeaders -> IO H.ResponseHeaders
-addDate dc rspidxhdr hdrs = case rspidxhdr ! idxDate of
+addDate dc rspidxhdr hdrs = case rspidxhdr ! fromEnum ResDate of
     Nothing -> do
         gmtdate <- D.getDate dc
         return $ (H.hDate, gmtdate) : hdrs
@@ -404,7 +439,7 @@
 warpVersion = showVersion Paths_warp.version
 
 addServer :: HeaderValue -> IndexedHeader -> H.ResponseHeaders -> H.ResponseHeaders
-addServer serverName rspidxhdr hdrs = case rspidxhdr ! idxServer of
+addServer serverName rspidxhdr hdrs = case rspidxhdr ! fromEnum ResServer of
     Nothing -> (H.hServer, serverName) : hdrs
     _       -> hdrs
 
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
@@ -2,6 +2,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
+{-# LANGUAGE PatternGuards #-}
 {-# OPTIONS_GHC -fno-warn-deprecations #-}
 
 module Network.Wai.Handler.Warp.Run where
@@ -29,6 +30,7 @@
 import Network.Wai.Handler.Warp.Counter
 import qualified Network.Wai.Handler.Warp.Date as D
 import qualified Network.Wai.Handler.Warp.FdCache as F
+import qualified Network.Wai.Handler.Warp.FileInfoCache as I
 import Network.Wai.Handler.Warp.HTTP2 (http2, isHTTP2)
 import Network.Wai.Handler.Warp.Header
 import Network.Wai.Handler.Warp.ReadInt
@@ -237,13 +239,18 @@
 runServeSettingsConnectionMakerSecure set getConnMaker serveConn = do
     settingsBeforeMainLoop set
     counter <- newCounter
-
-    D.withDateCache $ \dc ->
-        F.withFdCache fdCacheDurationInSeconds $ \fc ->
-            withTimeoutManager $ \tm ->
-                acceptConnection set getConnMaker serveConn dc fc tm counter
+    withII $ acceptConnection set getConnMaker serveConn counter
   where
+    withII action =
+        D.withDateCache $ \dc ->
+        F.withFdCache fdCacheDurationInSeconds $ \fc ->
+        I.withFileInfoCache fdFileInfoDurationInSeconds $ \get ->
+        withTimeoutManager $ \tm -> do
+            let ii0 = InternalInfo undefined tm fc get dc -- fixme: undefined
+            action ii0
+
     fdCacheDurationInSeconds = settingsFdCacheDuration set * 1000000
+    fdFileInfoDurationInSeconds = settingsFileInfoCacheDuration set * 1000000
     withTimeoutManager f = case settingsManager set of
         Just tm -> f tm
         Nothing -> bracket
@@ -267,12 +274,10 @@
 acceptConnection :: Settings
                  -> IO (IO (Connection, Transport), SockAddr)
                  -> ServeConnection
-                 -> D.DateCache
-                 -> Maybe F.MutableFdCache
-                 -> T.Manager
                  -> Counter
+                 -> InternalInfo
                  -> IO ()
-acceptConnection set getConnMaker serveConn dc fc tm counter = do
+acceptConnection set getConnMaker serveConn counter ii0 = do
     -- First mask all exceptions in acceptLoop. This is necessary to
     -- ensure that no async exception is throw between the call to
     -- acceptNewConnection and the registering of connClose.
@@ -294,7 +299,7 @@
         case mx of
             Nothing             -> return ()
             Just (mkConn, addr) -> do
-                fork set mkConn addr serveConn dc fc tm counter
+                fork set mkConn addr serveConn counter ii0
                 acceptLoop
 
     acceptNewConnection = do
@@ -319,12 +324,10 @@
      -> IO (Connection, Transport)
      -> SockAddr
      -> ServeConnection
-     -> D.DateCache
-     -> Maybe F.MutableFdCache
-     -> T.Manager
      -> Counter
+     -> InternalInfo
      -> IO ()
-fork set mkConn addr serveConn dc fc tm counter = settingsFork set $ \ unmask ->
+fork set mkConn addr serveConn counter ii0 = settingsFork set $ \ unmask ->
     -- Run the connection maker to get a new connection, and ensure
     -- that the connection is closed. If the mkConn call throws an
     -- exception, we will leak the connection. If the mkConn call is
@@ -339,9 +342,9 @@
 
     -- We need to register a timeout handler for this thread, and
     -- cancel that handler as soon as we exit.
-    bracket (T.registerKillThread tm) T.cancel $ \th ->
+    bracket (T.registerKillThread (timeoutManager ii0)) T.cancel $ \th ->
 
-    let ii = InternalInfo th tm fc dc
+    let ii = ii0 { threadHandle = th }
         -- We now have fully registered a connection close handler
         -- in the case of all exceptions, so it is safe to one
         -- again allow async exceptions.
@@ -443,12 +446,14 @@
 
     th = threadHandle ii
 
+    shouldSendErrorResponse se
+        | Just ConnectionClosedByPeer <- fromException se = False
+        | otherwise                                       = True
+
     sendErrorResponse addr istatus e = do
         status <- readIORef istatus
-        when status $ void $
-            sendResponse
-                (settingsServerName settings)
-                conn ii (dummyreq addr) defaultIndexRequestHeader (return S.empty) (errorResponse e)
+        when (shouldSendErrorResponse e && status) $ void $
+            sendResponse settings conn ii (dummyreq addr) defaultIndexRequestHeader (return S.empty) (errorResponse e)
 
     dummyreq addr = defaultRequest { remoteHost = addr }
 
@@ -480,9 +485,7 @@
             -- send more meaningful error messages to the user.
             -- However, it may affect performance.
             writeIORef istatus False
-            keepAlive <- sendResponse
-                (settingsServerName settings)
-                conn ii req idxhdr (readSource src) res
+            keepAlive <- sendResponse settings conn ii req idxhdr (readSource src) res
             writeIORef keepAliveRef keepAlive
             return ResponseReceived
         keepAlive <- readIORef keepAliveRef
diff --git a/Network/Wai/Handler/Warp/Settings.hs b/Network/Wai/Handler/Warp/Settings.hs
--- a/Network/Wai/Handler/Warp/Settings.hs
+++ b/Network/Wai/Handler/Warp/Settings.hs
@@ -48,6 +48,7 @@
     , settingsTimeout :: Int -- ^ Timeout value in seconds. Default value: 30
     , settingsManager :: Maybe Manager -- ^ Use an existing timeout manager instead of spawning a new one. If used, 'settingsTimeout' is ignored. Default is 'Nothing'
     , settingsFdCacheDuration :: Int -- ^ Cache duration time of file descriptors in seconds. 0 means that the cache mechanism is not used. Default value: 0
+    , settingsFileInfoCacheDuration :: Int -- ^ Cache duration time of file information in seconds. 0 means that the cache mechanism is not used. Default value: 0
     , settingsBeforeMainLoop :: IO ()
       -- ^ Code to run after the listening socket is ready but before entering
       -- the main event loop. Useful for signaling to tests that they can start
@@ -96,6 +97,10 @@
       -- ^ Whether to enable HTTP2 ALPN/upgrades. Default: True
       --
       -- Since 3.1.7.
+    , settingsLogger :: Request -> H.Status -> Maybe Integer -> IO ()
+      -- ^ A log function. Default: no action.
+      --
+      -- Since 3.X.X.
     }
 
 -- | Specify usage of the PROXY protocol.
@@ -119,6 +124,7 @@
     , settingsTimeout = 30
     , settingsManager = Nothing
     , settingsFdCacheDuration = 0
+    , settingsFileInfoCacheDuration = 0
     , settingsBeforeMainLoop = return ()
     , settingsFork = void . forkIOWithUnmask
     , settingsNoParsePath = False
@@ -128,6 +134,7 @@
     , settingsProxyProtocol = ProxyProtocolNone
     , settingsSlowlorisSize = 2048
     , settingsHTTP2Enabled = True
+    , settingsLogger = \_ _ _ -> return ()
     }
 
 -- | Apply the logic provided by 'defaultOnException' to determine if an
diff --git a/Network/Wai/Handler/Warp/Types.hs b/Network/Wai/Handler/Warp/Types.hs
--- a/Network/Wai/Handler/Warp/Types.hs
+++ b/Network/Wai/Handler/Warp/Types.hs
@@ -13,6 +13,7 @@
 import Foreign.Ptr (Ptr)
 import qualified Network.Wai.Handler.Warp.Date as D
 import qualified Network.Wai.Handler.Warp.FdCache as F
+import qualified Network.Wai.Handler.Warp.FileInfoCache as I
 import qualified Network.Wai.Handler.Warp.Timeout as T
 
 #ifndef WINDOWS
@@ -118,6 +119,7 @@
     threadHandle :: T.Handle
   , timeoutManager :: T.Manager
   , fdCacher :: Maybe F.MutableFdCache
+  , fileInfo :: FilePath -> IO I.FileInfo
   , dateCacher :: D.DateCache
   }
 
diff --git a/test/FileSpec.hs b/test/FileSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/FileSpec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module FileSpec (main, spec) where
+
+import Network.HTTP.Types
+import Network.Wai.Handler.Warp.File
+import Network.Wai.Handler.Warp.FileInfoCache
+import Network.Wai.Handler.Warp.Header
+
+import Test.Hspec
+
+main :: IO ()
+main = hspec spec
+
+testFileRange :: String
+              -> RequestHeaders -> FilePath
+              -> RspFileInfo
+              -> Spec
+testFileRange desc reqhs file ans = it desc $ do
+    finfo <- getInfo file
+    let WithBody s hs off len = ans
+        hs' = ("Last-Modified",fileInfoDate finfo) : hs
+        ans' = WithBody s hs' off len
+    conditionalRequest finfo [] (indexRequestHeader reqhs) `shouldBe` ans'
+
+spec :: Spec
+spec = do
+    describe "conditionalRequest" $ do
+        testFileRange
+            "gets a file size from file system"
+            [] "attic/hex"
+            $ WithBody ok200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
+        testFileRange
+            "gets a file size from file system and handles Range and returns Partical Content"
+            [("Range","bytes=2-14")] "attic/hex"
+            $ WithBody status206 [("Content-Range","bytes 2-14/16"),("Content-Length","13"),("Accept-Ranges","bytes")] 2 13
+        testFileRange
+            "truncates end point of range to file size"
+            [("Range","bytes=10-20")] "attic/hex"
+            $ WithBody status206 [("Content-Range","bytes 10-15/16"),("Content-Length","6"),("Accept-Ranges","bytes")] 10 6
+        testFileRange
+            "gets a file size from file system and handles Range and returns OK if Range means the entire"
+            [("Range:","bytes=0-15")] "attic/hex"
+            $ WithBody status200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
+
diff --git a/test/HTTP.hs b/test/HTTP.hs
--- a/test/HTTP.hs
+++ b/test/HTTP.hs
@@ -3,11 +3,14 @@
 module HTTP (
     sendGET
   , sendGETwH
+  , sendHEAD
+  , sendHEADwH
   , rspBody
   , rspCode
   , rspHeaders
   , getHeaderValue
   , HeaderName(..)
+  , mkHeader
   ) where
 
 import Network.HTTP
@@ -18,6 +21,12 @@
 
 sendGETwH :: String -> [Header] -> IO (Response String)
 sendGETwH url hdr = unResult $ simpleHTTP $ (getRequest url) { rqHeaders = hdr }
+
+sendHEAD :: String -> IO (Response String)
+sendHEAD url = sendHEADwH url []
+
+sendHEADwH :: String -> [Header] -> IO (Response String)
+sendHEADwH url hdr = unResult $ simpleHTTP $ (headRequest url) { rqHeaders = hdr }
 
 unResult :: IO (Result (Response String)) -> IO (Response String)
 unResult action = do
diff --git a/test/ResponseSpec.hs b/test/ResponseSpec.hs
--- a/test/ResponseSpec.hs
+++ b/test/ResponseSpec.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE OverloadedStrings #-}
+
 module ResponseSpec (main, spec) where
 
 import Control.Concurrent (threadDelay)
@@ -70,18 +71,6 @@
             _ -> Nothing
     range = "bytes " ++ show offset ++ "-" ++ show (offset + count - 1) ++ "/" ++ show size
 
-testFileRange :: String
-              -> Status -> ResponseHeaders -> FilePath
-              -> Maybe FilePart -> Maybe HeaderValue
-              -> Either String (Status, ResponseHeaders, Integer, Integer)
-              -> Spec
-testFileRange desc s rsphdr file mPart mRange ans = it desc $ do
-    eres <- fileRange s rsphdr file mPart mRange
-    let res = case eres of
-            Left  e   -> Left $ show e
-            Right r   -> Right r
-    res `shouldBe` ans
-
 spec :: Spec
 spec = do
     describe "preventing response splitting attack" $ do
@@ -124,37 +113,3 @@
         testPartial 16 2 2 "23"
         testPartial 16 0 2 "01"
         testPartial 16 3 8 "3456789a"
-
-    describe "fileRange" $ do
-        testFileRange
-            "gets a file size from file system"
-            status200 [] "attic/hex" Nothing Nothing
-            $ Right (status200,[("Content-Length","16"), ("Accept-Ranges","bytes")],0,16)
-        testFileRange
-            "gets an error if a file does not exist"
-            status200 [] "attic/nonexist" Nothing Nothing
-            $ Left "attic/nonexist: getFileStatus: does not exist (No such file or directory)"
-        testFileRange
-            "changes status if FileParts is specified"
-            status200 [] "attic/hex" (Just (FilePart 2 10 16)) Nothing
-            $ Right (status206,[("Content-Range", "bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")],2,10)
-        testFileRange
-            "does not change status and does not add Content-Range if FileParts means the entire"
-            status200 [] "attic/hex" (Just (FilePart 0 16 16)) Nothing
-            $ Right (status200,[("Content-Length","16"), ("Accept-Ranges","bytes")],0,16)
-        testFileRange
-            "gets a file size from file system and handles Range and returns Partical Content"
-            status200 [] "attic/hex" Nothing (Just "bytes=2-14")
-            $ Right (status206,[("Content-Range","bytes 2-14/16"),("Content-Length","13"),("Accept-Ranges","bytes")],2,13)
-        testFileRange
-            "truncates end point of range to file size"
-            status200 [] "attic/hex" Nothing (Just "bytes=10-20")
-            $ Right (status206,[("Content-Range","bytes 10-15/16"),("Content-Length","6"),("Accept-Ranges","bytes")],10,6)
-        testFileRange
-            "gets a file size from file system and handles Range and returns OK if Range means the entire"
-            status200 [] "attic/hex" Nothing (Just "bytes=0-15")
-            $ Right (status200,[("Content-Length","16"),("Accept-Ranges","bytes")],0,16)
-        testFileRange
-            "igores Range if FilePart is specified"
-            status200 [] "attic/hex" (Just (FilePart 2 10 16)) (Just "bytes=8-9")
-            $ Right (status206,[("Content-Range", "bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")],2,10)
diff --git a/test/RunSpec.hs b/test/RunSpec.hs
--- a/test/RunSpec.hs
+++ b/test/RunSpec.hs
@@ -377,6 +377,31 @@
             res <- sendGET $ "http://127.0.0.1:" ++ show port
             rspBody res `shouldBe` "HelloHelloHelloHello"
 
+    describe "head requests" $ do
+        let fp = "test/head-response"
+        let app req f =
+                f $ case pathInfo req of
+                    ["builder"] -> responseBuilder status200 [] $ error "should never be evaluated"
+                    ["streaming"] -> responseStream status200 [] $ \write _ ->
+                        write $ error "should never be evaluated"
+                    ["file"] -> responseFile status200 [] fp Nothing
+                    _ -> error "invalid path"
+        it "builder" $ withApp defaultSettings app $ \port -> do
+            res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/builder"]
+            rspBody res `shouldBe` ""
+        it "streaming" $ withApp defaultSettings app $ \port -> do
+            res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/streaming"]
+            rspBody res `shouldBe` ""
+        it "file, no range" $ withApp defaultSettings app $ \port -> do
+            bs <- S.readFile fp
+            res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/file"]
+            getHeaderValue HdrContentLength res `shouldBe` Just (show $ S.length bs)
+        it "file, with range" $ withApp defaultSettings app $ \port -> do
+            res <- sendHEADwH
+                (concat ["http://127.0.0.1:", show port, "/file"])
+                [mkHeader HdrRange "bytes=0-1"]
+            getHeaderValue HdrContentLength res `shouldBe` Just "2"
+
 consumeBody :: IO ByteString -> IO [ByteString]
 consumeBody body =
     loop id
diff --git a/test/head-response b/test/head-response
new file mode 100644
--- /dev/null
+++ b/test/head-response
@@ -0,0 +1,1 @@
+This is the body
diff --git a/warp.cabal b/warp.cabal
--- a/warp.cabal
+++ b/warp.cabal
@@ -1,5 +1,5 @@
 Name:                warp
-Version:             3.1.9
+Version:             3.1.10
 Synopsis:            A fast, light-weight web server for WAI applications.
 License:             MIT
 License-file:        LICENSE
@@ -18,6 +18,7 @@
 extra-source-files:  attic/hex
                      ChangeLog.md
                      README.md
+                     test/head-response
 
 Flag network-bytestring
     Default: False
@@ -52,6 +53,7 @@
                    , stm                       >= 2.3
                    , word8
                    , hashable
+                   , unordered-containers
   if flag(network-bytestring)
       Build-Depends: network                   >= 2.2.1.5  && < 2.2.3
                    , network-bytestring        >= 0.1.3    && < 0.1.4
@@ -64,6 +66,8 @@
                      Network.Wai.Handler.Warp.Counter
                      Network.Wai.Handler.Warp.Date
                      Network.Wai.Handler.Warp.FdCache
+                     Network.Wai.Handler.Warp.File
+                     Network.Wai.Handler.Warp.FileInfoCache
                      Network.Wai.Handler.Warp.HTTP2
                      Network.Wai.Handler.Warp.HTTP2.EncodeFrame
                      Network.Wai.Handler.Warp.HTTP2.HPACK
@@ -98,7 +102,6 @@
   if os(windows)
       Cpp-Options:   -DWINDOWS
       Build-Depends: time
-                   , old-locale
   else
       Build-Depends: unix
                    , http-date
@@ -118,6 +121,7 @@
                      ConduitSpec
                      ExceptionSpec
                      FdCacheSpec
+                     FileSpec
                      MultiMapSpec
                      ReadIntSpec
                      RequestSpec
@@ -151,7 +155,6 @@
                    , QuickCheck
                    , hspec                     >= 1.3
                    , time
-                   , old-locale
                    , text
                    , streaming-commons         >= 0.1.10
                    , async
@@ -163,6 +166,7 @@
                    , http2                     >= 1.3
                    , word8
                    , hashable
+                   , unordered-containers
 
   if (os(linux) || os(freebsd) || os(darwin)) && flag(allow-sendfilefd)
     Cpp-Options:   -DSENDFILEFD
