diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for network-unexceptional
 
+## 0.2.1.0 -- 2024-01-17
+
+* Add `receiveFromInterruptible"
+
 ## 0.2.0.0 -- 2023-09-18
 
 * Receive functions now fail with `EEOI` when the peer shuts down.
diff --git a/network-unexceptional.cabal b/network-unexceptional.cabal
--- a/network-unexceptional.cabal
+++ b/network-unexceptional.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: network-unexceptional
-version: 0.2.0.0
+version: 0.2.1.0
 synopsis: Network functions that do not throw exceptions
 description:
   Functions compatible with the Socket type from the network library that
@@ -31,7 +31,7 @@
     , error-codes >=0.1.3
     , byteslice >=0.2.8
     , network >=3.1
-    , primitive >=0.8
+    , primitive >=0.9
     , primitive-addr >=0.1.0.2
     , bytestring >=0.11.4
     , stm >=2.5.1
diff --git a/src/Network/Unexceptional/ByteArray.hs b/src/Network/Unexceptional/ByteArray.hs
--- a/src/Network/Unexceptional/ByteArray.hs
+++ b/src/Network/Unexceptional/ByteArray.hs
@@ -6,6 +6,7 @@
 
 module Network.Unexceptional.ByteArray
   ( receiveExactly
+  , receiveFromInterruptible
   ) where
 
 import Control.Exception (throwIO)
@@ -17,6 +18,7 @@
 import GHC.Conc (threadWaitWrite)
 import Network.Socket (Socket)
 import System.Posix.Types (Fd(Fd))
+import Control.Concurrent.STM (TVar)
 
 import qualified Network.Unexceptional.Types as Types
 import qualified Posix.Socket as X
@@ -38,3 +40,17 @@
     Right _ -> do
       dst' <- PM.unsafeFreezeByteArray dst
       pure (Right dst')
+
+receiveFromInterruptible ::
+     TVar Bool
+  -> Socket
+  -> Int -- ^ Maximum number of bytes to receive.
+  -> IO (Either Errno (ByteArray, S.SockAddr))
+receiveFromInterruptible !intr s !n = do
+  dst <- PM.newByteArray n
+  MB.receiveFromInterruptible intr s (MutableBytes dst 0 n) >>= \case
+    Left err -> pure (Left err)
+    Right (sz,sockAddr) -> do
+      PM.shrinkMutableByteArray dst sz
+      dst' <- PM.unsafeFreezeByteArray dst
+      pure (Right (dst', sockAddr))
diff --git a/src/Network/Unexceptional/MutableBytes.hs b/src/Network/Unexceptional/MutableBytes.hs
--- a/src/Network/Unexceptional/MutableBytes.hs
+++ b/src/Network/Unexceptional/MutableBytes.hs
@@ -1,14 +1,21 @@
 {-# language BangPatterns #-}
 {-# language DuplicateRecordFields #-}
-{-# language PatternSynonyms #-}
 {-# language LambdaCase #-}
 {-# language NamedFieldPuns #-}
+{-# language PatternSynonyms #-}
+{-# language ScopedTypeVariables #-}
 
+-- | Note: The functions that are designated as being intended for stream
+-- sockets convert a reception length of zero to an non-standard @EOI@ error
+-- code. Datagram reception functions do not do this.
 module Network.Unexceptional.MutableBytes
-  ( receive
+  ( -- * Stream Sockets
+    receive
   , receiveInterruptible
   , receiveExactly
   , receiveExactlyInterruptible
+    -- * Datagram Sockets
+  , receiveFromInterruptible
   ) where
 
 import Control.Applicative ((<|>))
@@ -18,14 +25,22 @@
 import Data.Bytes.Types (MutableBytes(MutableBytes))
 import Data.Functor (($>))
 import Data.Primitive (MutableByteArray)
+import Foreign.C.Types (CSize,CInt)
 import Foreign.C.Error (Errno)
-import Foreign.C.Error.Pattern (pattern EWOULDBLOCK,pattern EAGAIN)
 import Foreign.C.Error.Pattern (pattern EEOI)
+import Foreign.C.Error.Pattern (pattern EWOULDBLOCK,pattern EAGAIN)
+import Foreign.Storable (poke)
+import Foreign.Ptr (castPtr)
+import Foreign.Marshal.Alloc (allocaBytes,alloca)
 import GHC.Conc (threadWaitRead,threadWaitReadSTM)
-import GHC.Exts (RealWorld)
-import Network.Socket (Socket)
+import GHC.Exts (RealWorld,Ptr)
+import Network.Socket (Socket,SockAddr)
+import Network.Socket.Address (peekSocketAddress)
 import System.Posix.Types (Fd(Fd))
+import Data.Word (Word8)
 
+import qualified Data.Primitive as PM
+import qualified Data.Primitive.Ptr as PM
 import qualified Control.Concurrent.STM as STM
 import qualified Data.Bytes.Types
 import qualified Linux.Socket as X
@@ -47,6 +62,48 @@
       -- ready for reads.
       receiveLoop (Fd fd) array offset len
     else throwIO Types.NonpositiveReceptionSize
+
+-- | Receive bytes from a socket. Receives at most N bytes, where N
+-- is the size of the buffer. Returns the number of bytes that were
+-- actually received.
+receiveFromInterruptible ::
+     TVar Bool
+  -> Socket
+  -> MutableBytes RealWorld -- ^ Slice of a buffer
+  -> IO (Either Errno (Int, SockAddr))
+receiveFromInterruptible !interrupt s MutableBytes{array,offset,length=len} =
+  if len > 0
+    then S.withFdSocket s $ \fd -> do
+      -- We attempt the first receive without testing if the socket is
+      -- ready for reads.
+      receiveFromInterruptibleLoop interrupt (Fd fd) array offset len
+    else throwIO Types.NonpositiveReceptionSize
+
+receiveFromInterruptibleLoop ::
+     TVar Bool
+  -> Fd
+  -> MutableByteArray RealWorld
+  -> Int
+  -> Int
+  -> IO (Either Errno (Int, SockAddr))
+receiveFromInterruptibleLoop !intr !fd !dst !doff !dlen = 
+  X.uninterruptibleReceiveFromMutableByteArray fd dst doff (fromIntegral dlen :: CSize) mempty 128 >>= \case
+    Left e -> if e == EAGAIN || e == EWOULDBLOCK
+      then waitUntilReadable intr fd >>= \case
+        Ready -> receiveFromInterruptibleLoop intr fd dst doff dlen
+        Interrupted -> pure (Left EAGAIN)
+      else pure (Left e)
+    Right (sockAddrSz,X.SocketAddress sockAddr,recvSzC) -> do
+      let sockAddrSzI = fromIntegral sockAddrSz :: Int
+      pinned <- PM.newPinnedByteArray sockAddrSzI
+      PM.copyByteArray pinned 0 sockAddr 0 sockAddrSzI
+      pinned' <- PM.unsafeFreezeByteArray pinned
+      sockAddrNetwork <- PM.withByteArrayContents pinned' $ \ptr -> do
+        peekSocketAddress (castPtr ptr :: Ptr sa)
+      let recvSz = fromIntegral recvSzC :: Int
+       in case compare recvSz dlen of
+            GT -> throwIO Types.ReceivedTooManyBytes
+            _ -> pure (Right (recvSz, sockAddrNetwork))
 
 receiveInterruptible ::
      TVar Bool -- ^ Interrupt
