diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,7 @@
+## 0.4.9
+
+* Add RequestBody smart constructors `streamFile` and `streamFileObserved`, the latter with accompanying type `StreamFileStatus`.
+
 ## 0.4.8.1
 
 * Automatically call withSocketsDo everywhere [#107](https://github.com/snoyberg/http-client/issues/107)
diff --git a/Network/HTTP/Client.hs b/Network/HTTP/Client.hs
--- a/Network/HTTP/Client.hs
+++ b/Network/HTTP/Client.hs
@@ -146,6 +146,9 @@
     , Popper
     , NeedsPopper
     , GivesPopper
+    , streamFile
+    , observedStreamFile
+    , StreamFileStatus (..)
       -- * Response
     , Response
     , responseStatus
diff --git a/Network/HTTP/Client/MultipartFormData.hs b/Network/HTTP/Client/MultipartFormData.hs
--- a/Network/HTTP/Client/MultipartFormData.hs
+++ b/Network/HTTP/Client/MultipartFormData.hs
@@ -50,7 +50,7 @@
     ,renderPart
     ) where
 
-import Network.HTTP.Client
+import Network.HTTP.Client hiding (streamFile)
 import Network.Mime
 import Network.HTTP.Types (hContentType, methodPost, Header())
 import Data.Monoid ((<>))
diff --git a/Network/HTTP/Client/Request.hs b/Network/HTTP/Client/Request.hs
--- a/Network/HTTP/Client/Request.hs
+++ b/Network/HTTP/Client/Request.hs
@@ -21,12 +21,16 @@
     , requestBuilder
     , useDefaultTimeout
     , setQueryString
+    , streamFile
+    , observedStreamFile
     ) where
 
+import Data.Int (Int64)
 import Data.Maybe (fromMaybe, isJust)
 import Data.Monoid (mempty, mappend)
 import Data.String (IsString(..))
 import Data.Char (toLower)
+import Control.Applicative ((<$>))
 import Control.Monad (when, unless)
 import Numeric (showHex)
 
@@ -38,6 +42,7 @@
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Char8 as S8
 import qualified Data.ByteString.Lazy as L
+import Data.ByteString.Lazy.Internal (defaultChunkSize)
 
 import qualified Network.HTTP.Types as W
 import Network.URI (URI (..), URIAuth (..), parseURI, relativeTo, escapeURIString, isAllowedInURI)
@@ -57,6 +62,9 @@
 import Control.Monad.Catch (MonadThrow, throwM)
 import Data.IORef
 
+import System.IO (withBinaryFile, hTell, hFileSize, Handle, IOMode (ReadMode))
+
+
 -- | Convert a URL into a 'Request'.
 --
 -- This defaults some of the values in 'Request', such as setting 'method' to
@@ -406,3 +414,40 @@
 -- Since 0.3.6
 setQueryString :: [(S.ByteString, Maybe S.ByteString)] -> Request -> Request
 setQueryString qs req = req { queryString = W.renderQuery True qs }
+
+-- | Send a file as the request body.
+--
+-- It is expected that the file size does not change between calling
+-- `streamFile` and making any requests using this request body.
+--
+-- Since 0.4.9
+streamFile :: FilePath -> IO RequestBody
+streamFile = observedStreamFile (\_ -> return ())
+
+-- | Send a file as the request body, while observing streaming progress via
+-- a `PopObserver`. Observations are made between reading and sending a chunk.
+--
+-- It is expected that the file size does not change between calling
+-- `observedStreamFile` and making any requests using this request body.
+--
+-- Since 0.4.9
+observedStreamFile :: (StreamFileStatus -> IO ()) -> FilePath -> IO RequestBody
+observedStreamFile obs path = do
+    size <- fromIntegral <$> withBinaryFile path ReadMode hFileSize
+
+    let filePopper :: Handle -> Popper
+        filePopper h = do
+            bs <- S.hGetSome h defaultChunkSize
+            currentPosition <- fromIntegral <$> hTell h
+            obs $ StreamFileStatus
+                { fileSize = size
+                , readSoFar = currentPosition
+                , thisChunkSize = S.length bs
+                }
+            return bs
+
+        givesFilePopper :: GivesPopper ()
+        givesFilePopper k = withBinaryFile path ReadMode $ \h -> do
+            k (filePopper h)
+
+    return $ RequestBodyStream size givesFilePopper
diff --git a/Network/HTTP/Client/Types.hs b/Network/HTTP/Client/Types.hs
--- a/Network/HTTP/Client/Types.hs
+++ b/Network/HTTP/Client/Types.hs
@@ -28,6 +28,7 @@
     , ConnHost (..)
     , ConnKey (..)
     , ProxyOverride (..)
+    , StreamFileStatus (..)
     ) where
 
 import qualified Data.Typeable as T (Typeable)
@@ -597,4 +598,14 @@
 -- | @ConnKey@ consists of a hostname, a port and a @Bool@
 -- specifying whether to use SSL.
 data ConnKey = ConnKey ConnHost Int S.ByteString Int Bool
+    deriving (Eq, Show, Ord, T.Typeable)
+
+-- | Status of streaming a request body from a file.
+--
+-- Since 0.4.9
+data StreamFileStatus = StreamFileStatus
+    { fileSize :: Int64
+    , readSoFar :: Int64
+    , thisChunkSize :: Int
+    }
     deriving (Eq, Show, Ord, T.Typeable)
diff --git a/http-client.cabal b/http-client.cabal
--- a/http-client.cabal
+++ b/http-client.cabal
@@ -1,5 +1,5 @@
 name:                http-client
-version:             0.4.8.1
+version:             0.4.9
 synopsis:            An HTTP client engine, intended as a base layer for more user-friendly packages.
 description:         Hackage documentation generation is not reliable. For up to date documentation, please see: <http://www.stackage.org/package/http-client>.
 homepage:            https://github.com/snoyberg/http-client
@@ -95,6 +95,7 @@
                        Network.HTTP.Client.BodySpec
                        Network.HTTP.Client.HeadersSpec
                        Network.HTTP.Client.RequestSpec
+                       Network.HTTP.Client.RequestBodySpec
   build-depends:       base
                      , http-client
                      , hspec
@@ -113,3 +114,4 @@
                      , zlib
                      , async
                      , streaming-commons >= 0.1.1
+                     , directory
diff --git a/test-nonet/Network/HTTP/Client/RequestBodySpec.hs b/test-nonet/Network/HTTP/Client/RequestBodySpec.hs
new file mode 100644
--- /dev/null
+++ b/test-nonet/Network/HTTP/Client/RequestBodySpec.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Network.HTTP.Client.RequestBodySpec where
+
+import Control.Monad
+import Test.Hspec
+import Control.Exception
+import System.IO
+import Data.IORef
+import qualified Data.ByteString as BS
+import Network.HTTP.Client (streamFile, parseUrl, requestBody)
+import Network.HTTP.Client.Internal (dummyConnection, Connection, connectionWrite, requestBuilder)
+import System.Directory (getTemporaryDirectory)
+
+spec :: Spec
+spec = describe "streamFile" $ it "works" $ withTmpFile $ \(path, h) -> do
+    replicateM_ 5000 $ BS.hPut h "Hello, world!\r\n"
+    hClose h
+
+    withBinaryFile path ReadMode $ \h' -> do
+        conn <- verifyFileConnection h'
+
+        req0 <- parseUrl "http://example.com"
+        body <- streamFile path
+        let req = req0 { requestBody = body }
+
+        requestBuilder req conn
+        hIsEOF h' `shouldReturn` True
+  where
+    withTmpFile = bracket getTmpFile closeTmpFile
+    getTmpFile = do
+        tmp <- getTemporaryDirectory
+        openBinaryTempFile tmp "request-body-stream-file"
+    closeTmpFile (_, h) = hClose h
+
+    firstReadBS = "GET / HTTP/1.1\r\nHost: example.com\r\nAccept-Encoding: gzip\r\nContent-Length: 75000\r\n\r\n"
+
+    verifyFileConnection h = do
+        (conn, _, _) <- dummyConnection []
+        isFirstReadRef <- newIORef True
+        return conn
+            { connectionWrite = \bs -> do
+                isFirstRead <- readIORef isFirstReadRef
+                if isFirstRead
+                then do
+                    bs `shouldBe` firstReadBS
+                    writeIORef isFirstReadRef False
+                else do
+                    bs' <- BS.hGet h (BS.length bs)
+                    bs `shouldBe` bs'
+            }
