diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,6 @@
+## 0.6.2.0
+
+# Changes internal handling of the `X-Forwarded-For` header (avoids `Text` round-trips, tolerates invalid UTF-8) [#49](https://github.com/fpco/http-reverse-proxy/pull/49)
 
 ## 0.6.1.0
 
diff --git a/Network/HTTP/ReverseProxy.hs b/Network/HTTP/ReverseProxy.hs
--- a/Network/HTTP/ReverseProxy.hs
+++ b/Network/HTTP/ReverseProxy.hs
@@ -6,6 +6,7 @@
 {-# LANGUAGE LambdaCase            #-}
 {-# LANGUAGE TupleSections         #-}
 {-# LANGUAGE CPP                   #-}
+
 module Network.HTTP.ReverseProxy
     ( -- * Types
       ProxyDest (..)
@@ -63,7 +64,6 @@
 import qualified Data.Text.Lazy                 as TL
 import qualified Data.Text.Lazy.Encoding        as TLE
 import qualified Data.Text                      as T
-import qualified Data.Text.Encoding             as TE
 import           Data.Word8                     (isSpace, _colon, _cr)
 import           GHC.Generics                   (Generic)
 import           Network.HTTP.Client            (BodyReader, brRead)
@@ -79,6 +79,23 @@
     , pdPort :: !Int
     } deriving (Read, Show, Eq, Ord, Generic)
 
+-- | Trim optional whitespace (OWS) from both ends of a strict 'ByteString'.
+
+-- Per RFC 7230 §3.2.3, OWS = SP / HTAB. This function removes only leading
+-- and trailing space (0x20) and horizontal tab (0x09) bytes. It does not
+-- modify internal whitespace.
+
+-- On newer @bytestring@ versions (those that provide
+-- 'Data.ByteString.Char8.strip') this is a thin wrapper around that
+-- function; on older versions we provide a compatible fallback.
+trimOWS :: S8.ByteString -> S8.ByteString
+#if MIN_VERSION_bytestring(0,12,0)
+trimOWS = S8.strip
+#else
+trimOWS = S8.reverse . S8.dropWhile isOWS . S8.reverse . S8.dropWhile isOWS
+  where isOWS c = c == ' ' || c == '\t'
+#endif
+
 -- | Set up a reverse proxy server, which will have a minimal overhead.
 --
 -- This function uses raw sockets, parsing as little of the request as
@@ -362,7 +379,7 @@
     fromSocket = (("X-Real-IP", S8.pack $ showSockAddr $ WAI.remoteHost req):)
     fromForwardedFor = do
       h <- lookup "x-forwarded-for" (WAI.requestHeaders req)
-      listToMaybe $ map (TE.encodeUtf8 . T.strip) $ T.splitOn "," $ TE.decodeUtf8 h
+      listToMaybe $ map trimOWS $ S8.split ',' h
     addXRealIP =
         case wpsSetIpHeader wps of
             SIHFromSocket -> fromSocket
diff --git a/http-reverse-proxy.cabal b/http-reverse-proxy.cabal
--- a/http-reverse-proxy.cabal
+++ b/http-reverse-proxy.cabal
@@ -1,64 +1,74 @@
-name:                http-reverse-proxy
-version:             0.6.1.0
-synopsis:            Reverse proxy HTTP requests, either over raw sockets or with WAI
-description:         Provides a simple means of reverse-proxying HTTP requests. The raw approach uses the same technique as leveraged by keter, whereas the WAI approach performs full request/response parsing via WAI and http-conduit.
-homepage:            https://github.com/fpco/http-reverse-proxy
-license:             BSD3
-license-file:        LICENSE
-author:              Michael Snoyman
-maintainer:          michael@fpcomplete.com
-category:            Web
-build-type:          Simple
-cabal-version:       >=1.10
-extra-source-files:  README.md ChangeLog.md
+name:               http-reverse-proxy
+version:            0.6.2.0
+synopsis:
+  Reverse proxy HTTP requests, either over raw sockets or with WAI
 
+description:
+  Provides a simple means of reverse-proxying HTTP requests. The raw approach uses the same technique as leveraged by keter, whereas the WAI approach performs full request/response parsing via WAI and http-conduit.
+
+homepage:           https://github.com/fpco/http-reverse-proxy
+license:            BSD3
+license-file:       LICENSE
+author:             Michael Snoyman
+maintainer:         michael@fpcomplete.com
+category:           Web
+build-type:         Simple
+cabal-version:      >=1.10
+extra-source-files:
+  ChangeLog.md
+  README.md
+
 library
-  default-language:    Haskell2010
-  exposed-modules:     Network.HTTP.ReverseProxy
-  other-modules:       Paths_http_reverse_proxy
-  if impl(ghc < 8)
+  default-language: Haskell2010
+  exposed-modules:  Network.HTTP.ReverseProxy
+  other-modules:    Paths_http_reverse_proxy
+
+  if impl(ghc <8)
     buildable: False
-  build-depends:       base                   >= 4.11   && < 5
-                     , text                   >= 0.11
-                     , bytestring             >= 0.9
-                     , case-insensitive       >= 0.4
-                     , http-types             >= 0.6
-                     , word8                  >= 0.0
-                     , blaze-builder          >= 0.3
-                     , http-client            >= 0.3
-                     , wai                    >= 3.0
-                     , network
-                     , conduit                >= 1.3
-                     , conduit-extra
-                     , wai-logger
-                     , resourcet
-                     , containers
-                     , unliftio               >= 0.2
-                     , transformers
-                     , streaming-commons
 
+  build-depends:
+      base               >=4.11 && <5
+    , blaze-builder      >=0.3  && <0.5
+    , bytestring         >=0.9  && <0.13
+    , case-insensitive   >=0.4  && <1.3
+    , conduit            >=1.3  && <1.4
+    , conduit-extra      <1.4
+    , containers         <0.9
+    , http-client        >=0.3  && <0.8
+    , http-types         >=0.6  && <0.13
+    , network            <3.3
+    , resourcet          <1.4
+    , streaming-commons  <0.3
+    , text               >=0.11 && <2.2
+    , transformers       <0.7
+    , unliftio           >=0.2  && <0.3
+    , wai                >=3.0  && <3.3
+    , wai-logger         >=2.0  && <2.6
+    , word8              >=0.0  && <0.2
+
 test-suite test
-    default-language:    Haskell2010
-    type: exitcode-stdio-1.0
-    main-is: main.hs
-    hs-source-dirs: test
-    build-depends: base < 10
-                 , http-reverse-proxy
-                 , wai
-                 , http-types
-                 , hspec                      >= 1.3
-                 , warp                       >= 2.1
-                 , bytestring
-                 , conduit                    >= 1.1
-                 , conduit-extra
-                 , blaze-builder
-                 , transformers
-                 , unliftio
-                 , network
-                 , http-conduit               >= 2.3
-                 , resourcet
-                 , streaming-commons
+  default-language: Haskell2010
+  type:             exitcode-stdio-1.0
+  main-is:          main.hs
+  hs-source-dirs:   test
+  build-depends:
+      base                <10
+    , blaze-builder
+    , bytestring
+    , conduit             >=1.1
+    , conduit-extra
+    , hspec               >=1.3
+    , http-conduit        >=2.3
+    , http-reverse-proxy
+    , http-types
+    , network
+    , resourcet
+    , streaming-commons
+    , transformers
+    , unliftio
+    , wai
+    , warp                >=2.1
 
 source-repository head
   type:     git
-  location: git://github.com/fpco/http-reverse-proxy.git
+  location: https://github.com/fpco/http-reverse-proxy.git
diff --git a/test/main.hs b/test/main.hs
--- a/test/main.hs
+++ b/test/main.hs
@@ -211,6 +211,21 @@
                 withCApp (rawTcpProxyTo (ProxyDest "127.0.0.1" port3)) $ \port4 -> do
                     lbs <- httpWithForwardedFor $ "http://127.0.0.1:" ++ show port4
                     lbs `shouldBe` "127.0.0.1"
+        it "doesn't fail on invalid utf8 in x-forwarded-for header" $
+            let getRealIp req = L8.fromStrict $ fromMaybe "" $ lookup "x-real-ip" (requestHeaders req)
+                httpWithForwardedFor url = liftIO $ do
+                  man <- HC.newManager HC.tlsManagerSettings
+                  oreq <- liftIO $ HC.parseUrlThrow url
+                  let req = oreq { HC.requestHeaders = [("X-Forwarded-For", "\xbf\xf0\x9f\x92\xa1"), ("Connection", "close")] }
+                  HC.responseBody <$> HC.httpLbs req man
+                waiProxyTo' getDest onError = waiProxyToSettings getDest defaultWaiProxySettings { wpsOnExc = onError, wpsSetIpHeader = SIHFromHeader }
+             in withMan $ \manager ->
+                withWApp (\r f -> f $ responseLBS status200 [] $ getRealIp r ) $ \port1 ->
+                withWApp (waiProxyTo' (const $ return $ WPRProxyDest $ ProxyDest "127.0.0.1" port1) defaultOnExc manager) $ \port2 ->
+                withCApp (rawProxyTo (const $ return $ Right $ ProxyDest "127.0.0.1" port2)) $ \port3 ->
+                withCApp (rawTcpProxyTo (ProxyDest "127.0.0.1" port3)) $ \port4 -> do
+                    lbs <- httpWithForwardedFor $ "http://127.0.0.1:" ++ show port4
+                    lbs `shouldBe` "\xbf\xf0\x9f\x92\xa1"
         it "performs log action" $
           let ioref :: IO (IORef Int)
               ioref = newIORef 1
