diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,13 @@
 # ChangeLog for http-semantics
 
+## 0.4.2
+
+* `defaultReadNWith` is `defaultReadN` with an `STM` action beside each
+  receive.  When it fires the read answers with the empty string, which is
+  what `ReadN` already means by end of input, so a reader blocked in it can
+  be given up on without closing the socket under it or throwing into it --
+  neither of which works everywhere.
+
 ## 0.4.1
 
 * Add `auxSendInformational` to the server `Aux` for sending 1xx informational
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
@@ -2,9 +2,11 @@
     -- * Reading n bytes
     ReadN,
     defaultReadN,
+    defaultReadNWith,
 )
 where
 
+import Control.Concurrent.STM
 import qualified Data.ByteString as B
 import Data.IORef
 import Network.Socket
@@ -19,13 +21,25 @@
 -- (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
+defaultReadN s ref = defaultReadNWith s ref retry
+
+-- | 'defaultReadN' which the caller can give up on.  The 'STM' action is
+-- waited for beside each receive; when it fires, the read answers with the
+-- empty string, which is how 'ReadN' already says that there is nothing more.
+--
+-- /NOTE/: the bytes read so far are discarded, so this ends a connection
+-- rather than pausing one.  The caller cannot tell a give-up from the peer
+-- hanging up; whoever fired the 'STM' action knows which it was.
+--
+-- @since 0.4.2
+defaultReadNWith :: Socket -> IORef (Maybe B.ByteString) -> STM a -> ReadN
+defaultReadNWith _ _ _ 0 = return B.empty
+defaultReadNWith s ref giveUp n = do
     mbs <- readIORef ref
     writeIORef ref Nothing
     case mbs of
         Nothing -> do
-            bs <- N.recv s n
+            bs <- recv1 n
             if B.null bs
                 then return B.empty
                 else
@@ -40,9 +54,10 @@
                 return bs0
             | otherwise -> loop bs
   where
+    recv1 n' = either (const B.empty) id <$> N.recvWith s n' giveUp
     loop bs = do
         let n' = n - B.length bs
-        bs1 <- N.recv s n'
+        bs1 <- recv1 n'
         if B.null bs1
             then return B.empty
             else do
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
@@ -86,16 +86,20 @@
     -- 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.
+    -- for this purpose.
     , 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
+    -- Cancelling a streams sends a @RST_STREAM@ to the network peer, which
+    -- tells them that they should stop sending us messages. As such, it may
+    -- still be necessary to call 'outBodyCancel' after 'outBodyPushFinal',
+    -- which merely tells the peer that /we/ will no longer send /them/ any more
+    -- messages.
+    --
+    -- 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@.
     --
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.4.1
+version:         0.4.2
 license:         BSD-3-Clause
 license-file:    LICENSE
 maintainer:      Kazu Yamamoto <kazu@iij.ad.jp>
@@ -44,7 +44,8 @@
         bytestring >=0.10,
         case-insensitive,
         http-types >=0.12 && <0.13,
-        network,
+        network >=3.2.10,
         network-byte-order,
+        stm,
         time-manager >= 0.2.4,
         utf8-string
