diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # ChangeLog for http-semantics
 
+## 0.3.0
+
+* Breaking change: fillFileBodyGetNext takes Sentinel instead of
+  IO () to close files on time.
+
 ## 0.2.1
 
 * Add outBodyCancel to OutBodyIface
diff --git a/Network/HTTP/Semantics/Client.hs b/Network/HTTP/Semantics/Client.hs
--- a/Network/HTTP/Semantics/Client.hs
+++ b/Network/HTTP/Semantics/Client.hs
@@ -17,7 +17,7 @@
     requestBuilder,
 
     -- ** Generalized streaming interface
-    OutBodyIface(..),
+    OutBodyIface (..),
     requestStreamingIface,
 
     -- ** Trailers maker
diff --git a/Network/HTTP/Semantics/FillBuf.hs b/Network/HTTP/Semantics/FillBuf.hs
--- a/Network/HTTP/Semantics/FillBuf.hs
+++ b/Network/HTTP/Semantics/FillBuf.hs
@@ -35,7 +35,7 @@
 -- In @http2@ this will be used to construct a single HTTP2 @DATA@ frame
 -- (see discussion of the maximum number of bytes, below).
 type DynaNext =
-       Buffer
+    Buffer
     -- ^ Write buffer
     -> Int
     -- ^ Maximum number of bytes we are allowed to write
@@ -76,15 +76,14 @@
 -- | Action to run prior to terminating the stream
 type CleanupStream = IO ()
 
-data IsEndOfStream =
-    -- | The stream is not yet terminated
-    NotEndOfStream
-
-    -- | The stream is terminated
-    --
-    -- In addition to indicating that the stream is terminated, we can also
-    -- specify an optional `Cleanup` handler to be run.
-  | EndOfStream (Maybe CleanupStream)
+data IsEndOfStream
+    = -- | The stream is not yet terminated
+      NotEndOfStream
+    | -- | The stream is terminated
+      --
+      -- In addition to indicating that the stream is terminated, we can also
+      -- specify an optional `Cleanup` handler to be run.
+      EndOfStream (Maybe CleanupStream)
 
 ----------------------------------------------------------------
 
@@ -94,11 +93,11 @@
     return $ nextForBuilder len signal
 
 fillFileBodyGetNext
-    :: PositionRead -> FileOffset -> ByteCount -> IO () -> DynaNext
-fillFileBodyGetNext pread start bytecount refresh buf room = do
+    :: PositionRead -> FileOffset -> ByteCount -> Sentinel -> DynaNext
+fillFileBodyGetNext pread start bytecount sentinel buf room = do
     len <- pread start (mini room bytecount) buf
     let len' = fromIntegral len
-    return $ nextForFile len' pread (start + len) (bytecount - len) refresh
+    nextForFile len' pread (start + len) (bytecount - len) sentinel
 
 fillStreamBodyGetNext :: IO (Maybe StreamingChunk) -> DynaNext
 fillStreamBodyGetNext takeQ = loop 0
@@ -116,10 +115,10 @@
 fillBufBuilderOne minReq writer buf0 room = do
     if room >= minReq
         then do
-          (len, signal) <- writer buf0 room
-          return $ nextForBuilder len signal
+            (len, signal) <- writer buf0 room
+            return $ nextForBuilder len signal
         else do
-          return $ Next 0 True (Just $ fillBufBuilderOne minReq writer)
+            return $ Next 0 True (Just $ fillBufBuilderOne minReq writer)
 
 fillBufBuilderTwo :: ByteString -> B.BufferWriter -> DynaNext
 fillBufBuilderTwo bs writer buf0 room
@@ -202,10 +201,10 @@
         let enoughRoom = maybe True (room >=) mMinReq
         if enoughRoom
             then do
-              writeResult <- writer buf room
-              ranWriter writeResult total buf room
+                writeResult <- writer buf room
+                ranWriter writeResult total buf room
             else do
-              return $ Next total True (Just $ goMore mMinReq writer 0)
+                return $ Next total True (Just $ goMore mMinReq writer 0)
 
     goChunk :: ByteString -> B.BufferWriter -> NextWithTotal
     goChunk bs writer = \total buf room ->
@@ -221,19 +220,25 @@
 
 ----------------------------------------------------------------
 
-fillBufFile :: PositionRead -> FileOffset -> ByteCount -> IO () -> DynaNext
-fillBufFile pread start bytes refresh buf room = do
+fillBufFile :: PositionRead -> FileOffset -> ByteCount -> Sentinel -> DynaNext
+fillBufFile pread start bytes sentinel buf room = do
     len <- pread start (mini room bytes) buf
-    refresh
+    case sentinel of
+        Refresher refresh -> refresh
+        _ -> return ()
     let len' = fromIntegral len
-    return $ nextForFile len' pread (start + len) (bytes - len) refresh
+    nextForFile len' pread (start + len) (bytes - len) sentinel
 
 nextForFile
-    :: BytesFilled -> PositionRead -> FileOffset -> ByteCount -> IO () -> Next
-nextForFile 0 _ _ _ _ = Next 0 True Nothing -- let's flush
-nextForFile len _ _ 0 _ = Next len False Nothing
+    :: BytesFilled -> PositionRead -> FileOffset -> ByteCount -> Sentinel -> IO Next
+nextForFile 0 _ _ _ _ = return $ Next 0 True Nothing -- let's flush
+nextForFile len _ _ 0 sentinel = do
+    case sentinel of
+        Closer close -> close
+        _ -> return ()
+    return $ Next len False Nothing
 nextForFile len pread start bytes refresh =
-    Next len False $ Just $ fillBufFile pread start bytes refresh
+    return $ Next len False $ Just $ fillBufFile pread start bytes refresh
 
 {-# INLINE mini #-}
 mini :: Int -> Int64 -> Int64
diff --git a/Network/HTTP/Semantics/Server.hs b/Network/HTTP/Semantics/Server.hs
--- a/Network/HTTP/Semantics/Server.hs
+++ b/Network/HTTP/Semantics/Server.hs
@@ -34,7 +34,7 @@
     responseBuilder,
 
     -- ** Generalized streaming interface
-    OutBodyIface(..),
+    OutBodyIface (..),
     responseStreamingIface,
 
     -- ** Accessing response
diff --git a/http-semantics.cabal b/http-semantics.cabal
--- a/http-semantics.cabal
+++ b/http-semantics.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.0
 name:            http-semantics
-version:         0.2.1
+version:         0.3.0
 license:         BSD-3-Clause
 license-file:    LICENSE
 maintainer:      Kazu Yamamoto <kazu@iij.ad.jp>
