diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,13 @@
 # ChangeLog for http-semantics
 
+## 0.2.1
+
+* Add outBodyCancel to OutBodyIface
+  [#11](https://github.com/kazu-yamamoto/http-semantics/pull/11)
+* Documentation improvement.
+  [#10](https://github.com/kazu-yamamoto/http-semantics/pull/10)
+  [#11](https://github.com/kazu-yamamoto/http-semantics/pull/11)
+
 ## 0.2.0
 
 * Introduce `responseStreamingIface`
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
@@ -16,6 +16,7 @@
     fillStreamBodyGetNext,
 ) where
 
+import Control.Exception (SomeException)
 import Control.Monad
 import qualified Data.ByteString as BS
 import Data.ByteString.Builder (Builder)
@@ -29,8 +30,24 @@
 
 ----------------------------------------------------------------
 
--- type DynaNext = Buffer -> BufferSize -> WindowSize -> IO Next
-type DynaNext = Buffer -> Int -> IO Next
+-- | Write part of a streaming response to the write buffer
+--
+-- 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
+    -- ^ Write buffer
+    -> Int
+    -- ^ Maximum number of bytes we are allowed to write
+    --
+    -- In @http2@, this maximum will be set to the space left in the write
+    -- buffer. Implicitly this also means that this maximum cannot exceed the
+    -- maximum size of a HTTP2 frame, since in @http2@ the size of the write
+    -- buffer is also used to set @SETTINGS_MAX_FRAME_SIZE@ (see
+    -- @confBufferSize@).
+    -> IO Next
+    -- ^ Information on the data written, and on how to continue if not all data
+    -- was written
 
 type BytesFilled = Int
 
@@ -39,12 +56,15 @@
         BytesFilled -- payload length
         Bool -- require flushing
         (Maybe DynaNext)
+    | CancelNext (Maybe SomeException)
 
 ----------------------------------------------------------------
 
 data StreamingChunk
     = -- | Indicate that the stream is finished
       StreamingFinished (Maybe CleanupStream)
+    | -- | Indicate that the stream is cancelled
+      StreamingCancelled (Maybe SomeException)
     | -- | Flush the stream
       --
       -- This will cause the write buffer to be written to the network socket,
@@ -124,6 +144,10 @@
 ----------------------------------------------------------------
 
 -- | Like 'DynaNext', but with additional argument indicating total bytes written
+--
+-- Since @http2@ uses @DynaNext@ to construct a /single/ @DATA@ frame, the
+-- \"total number of bytes written\" refers to the current size of the payload
+-- of that @DATA@ frame.
 type NextWithTotal = Int -> DynaNext
 
 -- | Run the chunk, then continue as specified, unless streaming is finished
@@ -131,6 +155,7 @@
 runStreamingChunk chunk next =
     case chunk of
         StreamingFinished mdec -> finished mdec
+        StreamingCancelled mErr -> cancel mErr
         StreamingFlush -> flush
         StreamingBuilder builder NotEndOfStream -> runStreamingBuilder builder next
         StreamingBuilder builder (EndOfStream mdec) -> runStreamingBuilder builder (finished mdec)
@@ -143,6 +168,17 @@
     flush :: NextWithTotal
     flush = \total _buf _room -> do
         return $ Next total True (Just $ next 0)
+
+    -- Cancel streaming
+    --
+    -- The @_total@ number of bytes written refers to the @DATA@ frame currently
+    -- under construction, but not yet sent (see discussion at 'DynaNext' and
+    -- 'NextWithTotal'). Moreover, the documentation of 'outBodyCancel'
+    -- explicitly states that such a partially constructed frame, if one exists,
+    -- will be discarded on cancellation. We can therefore simply ignore
+    -- @_total@ here.
+    cancel :: Maybe SomeException -> NextWithTotal
+    cancel mErr = \_total _buf _room -> pure $ CancelNext mErr
 
 -- | Run 'Builder' until completion, then continue as specified
 runStreamingBuilder :: Builder -> NextWithTotal -> NextWithTotal
diff --git a/Network/HTTP/Semantics/ReadN.hs b/Network/HTTP/Semantics/ReadN.hs
--- a/Network/HTTP/Semantics/ReadN.hs
+++ b/Network/HTTP/Semantics/ReadN.hs
@@ -14,6 +14,10 @@
 type ReadN = Int -> IO B.ByteString
 
 -- | Naive implementation for readN.
+--
+-- /NOTE/: This function is intended to be used by a single thread only.
+-- (It is probably quite rare anyway to want concurrent reads from the /same/
+-- network socket.)
 defaultReadN :: Socket -> IORef (Maybe B.ByteString) -> ReadN
 defaultReadN _ _ 0 = return B.empty
 defaultReadN s ref n = do
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
@@ -80,6 +80,8 @@
 --   should give them to the sending function.
 --   The sending function would throw exceptions so that
 --   they can be logged.
+--
+--   The sending function must only be called once.
 type Server = Request -> Aux -> (Response -> [PushPromise] -> IO ()) -> IO ()
 
 -- | HTTP/2 push promise or sever push.
diff --git a/Network/HTTP/Semantics/Types.hs b/Network/HTTP/Semantics/Types.hs
--- a/Network/HTTP/Semantics/Types.hs
+++ b/Network/HTTP/Semantics/Types.hs
@@ -26,6 +26,7 @@
     Path,
 ) where
 
+import Control.Exception (SomeException)
 import Data.ByteString.Builder (Builder)
 import Data.IORef
 import Data.Int (Int64)
@@ -73,14 +74,43 @@
     -- streaming body.
     , outBodyPush :: Builder -> IO ()
     -- ^ Push a new chunk
+    --
+    -- In @http2@, there is no direct correspondence between chunks and the
+    -- resulting @DATA@ frames sent: the chunks are collected (written to an
+    -- internal write buffer) until we can fill a frame.
+    --
+    -- See also 'outBodyFlush'.
     , outBodyPushFinal :: Builder -> IO ()
     -- ^ Push the final chunk
     --
-    -- Using this function instead of 'outBodyPush' can be used to guarantee that the final
-    -- HTTP2 DATA frame is marked end-of-stream; with 'outBodyPush' it may happen that
-    -- an additional empty DATA frame is used for this purpose.
+    -- Using this function instead of 'outBodyPush' can be used to guarantee
+    -- that the final HTTP2 DATA frame is marked end-of-stream; with
+    -- 'outBodyPush' it may happen that an additional empty DATA frame is used
+    -- for this purpose. Additionally, after calling this function,
+    -- 'outBodyCancel' will be a no-op.
+    , outBodyCancel :: Maybe SomeException -> IO ()
+    -- ^ Cancel the stream
+    --
+    -- Sends a @RST_STREAM@ to the peer. If cancelling as the result of an
+    -- exception, a 'Just' should be provided which specifies the exception
+    -- which will be stored locally as the reason for cancelling the stream; in
+    -- this case, the error code sent with the @RST_STREAM@ will be
+    -- @INTERNAL_ERROR@ (see
+    -- <https://datatracker.ietf.org/doc/html/rfc7540#section-7>). If 'Nothing'
+    -- is given, the error code will be @CANCEL@.
+    --
+    -- If there is a partially constructed @DATA@ frame at the time of
+    -- cancellation, this frame is discarded. If this is undesirable, you should
+    -- call 'outBodyFlush' prior to cancelling.
     , outBodyFlush :: IO ()
     -- ^ Flush
+    --
+    -- This can be used to emit a DATA frame with the data collected so far
+    -- (using 'outBodyPush'), even if that DATA frame has not yet reached the
+    -- maximum frame size. Calling 'outBodyFlush' unnecessarily can therefore
+    -- result in excessive overhead from frame headers.
+    --
+    -- If no data is available to send, this is a no-op.
     }
 
 -- | Input object
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.0
+version:         0.2.1
 license:         BSD-3-Clause
 license-file:    LICENSE
 maintainer:      Kazu Yamamoto <kazu@iij.ad.jp>
