diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Revision history for wai-handler-hal
 
+## 0.5.1.0 -- 2026-11-09
+
+- Responses with a `Content-Encoding` other than `identity` (e.g.,
+  `gzip` from compression middleware) are now always base64-encoded,
+  regardless of `binaryMediaTypes`. Previously, these binary bodies
+  were decoded as UTF-8 text and corrupted. Your API's
+  `binaryMediaTypes` must cover these responses' content types (often
+  by using `*/*`) for API Gateway to decode them.
+- `bytestring >=0.10.12` is now required.
+
 ## 0.5.0.1 -- 2026-09-29
 
 - Use `defaultRequest` from `wai` to construct wai request to stay compatible
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -65,6 +65,11 @@
 * The Lambda is never told the HTTP version that the client uses when
   talking with the API Gateway. We assume HTTP 1.1.
 
+* Responses with a `Content-Encoding` other than `identity` (e.g.,
+  from `gzip` middleware) are always base64-encoded. Make sure your
+  API's `binaryMediaTypes` covers their content types (often by using
+  `*/*`), or prefer API Gateway's built-in response compression.
+
 ## Packaging
 
 Lambda functions are packaged in one of two ways: as `.zip` files or
diff --git a/src/Network/Wai/Handler/Hal.hs b/src/Network/Wai/Handler/Hal.hs
--- a/src/Network/Wai/Handler/Hal.hs
+++ b/src/Network/Wai/Handler/Hal.hs
@@ -50,12 +50,14 @@
   )
 import qualified AWS.Lambda.Events.ApiGateway.ProxyResponse as HalResponse
 import Control.Exception (IOException, tryJust)
+import Control.Monad (guard)
 import Control.Monad.IO.Class (MonadIO (..))
 import Data.ByteString (ByteString)
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Base64 as Base64
 import qualified Data.ByteString.Builder as Builder
 import qualified Data.ByteString.Builder.Extra as Builder
+import qualified Data.ByteString.Char8 as Char8
 import qualified Data.ByteString.Lazy as LByteString
 import qualified Data.CaseInsensitive as CI
 import Data.Function ((&))
@@ -75,12 +77,14 @@
 import Network.HTTP.Types.Header
   ( HeaderName,
     ResponseHeaders,
+    hContentEncoding,
     hContentType,
     hHost,
     hRange,
     hReferer,
     hUserAgent,
   )
+import Network.HTTP.Types.Status (Status)
 import Network.HTTP.Types.URI
   ( Query,
     QueryItem,
@@ -135,6 +139,15 @@
     -- If you set @binaryMediaTypes@ in your API, you should override
     -- the default (empty) list to match.
     --
+    -- Responses with a @Content-Encoding@ other than @identity@
+    -- (e.g., @gzip@ from compression middleware) are /always/
+    -- base64-encoded, regardless of this list, because their bodies
+    -- are binary. Your API's @binaryMediaTypes@ must then cover
+    -- those responses' content types (often by using @*/*@), or API
+    -- Gateway will pass the base64 text through to the client. API
+    -- Gateway's own response compression is usually a better choice
+    -- than compressing inside your Lambda.
+    --
     -- /See:/ [Content type conversion in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-workflow.html)
     -- in the [Amazon API Gateway Developer Guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/).
     binaryMediaTypes :: [MediaType]
@@ -335,18 +348,11 @@
 -- | Convert a WAI 'Wai.Response' into a hal
 -- 'HalResponse.ProxyResponse'.
 fromWaiResponse :: Options -> Wai.Response -> IO HalResponse.ProxyResponse
-fromWaiResponse opts (Wai.ResponseFile status headers path mFilePart) = do
-  fileData <- readFilePart path mFilePart
-  pure
-    . addHeaders headers
-    . HalResponse.response status
-    . createProxyBody opts (getContentType headers)
-    $ fileData
+fromWaiResponse opts (Wai.ResponseFile status headers path mFilePart) =
+  createProxyResponse opts status headers <$> readFilePart path mFilePart
 fromWaiResponse opts (Wai.ResponseBuilder status headers builder) =
   pure
-    . addHeaders headers
-    . HalResponse.response status
-    . createProxyBody opts (getContentType headers)
+    . createProxyResponse opts status headers
     . LByteString.toStrict
     $ Builder.toLazyByteString builder
 fromWaiResponse opts (Wai.ResponseStream status headers stream) = do
@@ -366,19 +372,44 @@
       hSeek h AbsoluteSeek offset
       ByteString.hGet h $ fromIntegral count
 
-createProxyBody :: Options -> MediaType -> ByteString -> HalResponse.ProxyBody
-createProxyBody opts contentType body =
-  HalResponse.ProxyBody
-    { HalResponse.contentType = Text.decodeUtf8Lenient $ renderHeader contentType,
-      HalResponse.serialized =
-        if isBase64Encoded
-          then Text.decodeUtf8Lenient $ Base64.encode body
-          else Text.decodeUtf8Lenient body,
-      HalResponse.isBase64Encoded
-    }
+createProxyResponse ::
+  Options ->
+  Status ->
+  ResponseHeaders ->
+  ByteString ->
+  HalResponse.ProxyResponse
+createProxyResponse opts status headers body =
+  addHeaders headers . HalResponse.response status $
+    HalResponse.ProxyBody
+      { HalResponse.contentType =
+          Text.decodeUtf8Lenient $ renderHeader contentType,
+        HalResponse.serialized =
+          Text.decodeUtf8Lenient $
+            if isBase64Encoded then Base64.encode body else body,
+        HalResponse.isBase64Encoded
+      }
   where
-    isBase64Encoded = any (contentType `matches`) $ binaryMediaTypes opts
+    -- Try to find the content-type of a response. If we can't, use
+    -- @"application/octet-stream"@.
+    contentType :: MediaType
+    contentType =
+      fromMaybe "application/octet-stream" $
+        lookup hContentType headers >>= parseAccept
 
+    isBase64Encoded =
+      any (contentType `matches`) (binaryMediaTypes opts)
+        || hasNonIdentityContentEncoding
+
+    -- Does the response declare a @Content-Encoding@ other than
+    -- @identity@? Such bodies (e.g., gzip-compressed) are binary
+    -- regardless of their @Content-Type@.
+    hasNonIdentityContentEncoding = any (/= "identity") $ do
+      (name, value) <- headers
+      guard $ name == hContentEncoding
+      token <- Char8.strip <$> Char8.split ',' value
+      guard . not $ ByteString.null token
+      [CI.mk token]
+
 addHeaders ::
   ResponseHeaders -> HalResponse.ProxyResponse -> HalResponse.ProxyResponse
 addHeaders headers response = foldl' addHeader response headers
@@ -388,10 +419,3 @@
         (Text.decodeUtf8Lenient $ CI.original hName)
         (Text.decodeUtf8Lenient hValue)
         r
-
--- | Try to find the content-type of a response, given the response
--- headers. If we can't, return @"application/octet-stream"@.
-getContentType :: ResponseHeaders -> MediaType
-getContentType headers =
-  fromMaybe "application/octet-stream" $
-    lookup hContentType headers >>= parseAccept
diff --git a/test/Network/Wai/Handler/HalTest.hs b/test/Network/Wai/Handler/HalTest.hs
--- a/test/Network/Wai/Handler/HalTest.hs
+++ b/test/Network/Wai/Handler/HalTest.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
 
 module Network.Wai.Handler.HalTest where
 
@@ -10,16 +11,19 @@
     ProxyResponse (..),
   )
 import Data.Aeson (eitherDecodeFileStrict')
+import Data.ByteString (ByteString)
 import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Lazy as LBS
+import Data.Functor ((<&>))
 import qualified Data.Text.Encoding as T
 import qualified Data.Text.Lazy.Encoding as TL
 import Data.Void (Void)
-import Network.HTTP.Types (hContentType, ok200)
+import Network.HTTP.Types (hContentEncoding, hContentType, ok200)
 import Network.Wai (Response, responseLBS)
 import Network.Wai.Handler.Hal
-import Test.Tasty (TestTree)
+import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.Golden (goldenVsString)
-import Test.Tasty.HUnit (Assertion, assertEqual)
+import Test.Tasty.HUnit (Assertion, assertEqual, testCase)
 import Text.Pretty.Simple (pShowNoColor)
 
 test_ConvertProxyRequest :: TestTree
@@ -53,3 +57,61 @@
 
 helloWorld :: Response
 helloWorld = responseLBS ok200 [(hContentType, "text/plain")] "Hello, World!"
+
+unit_GzipResponseIsBase64Encoded :: Assertion
+unit_GzipResponseIsBase64Encoded = do
+  let gzipBytes = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xff\xfe"
+  ProxyResponse {body = ProxyBody {..}} <-
+    fromWaiResponse defaultOptions $
+      responseLBS
+        ok200
+        [(hContentType, "text/html"), (hContentEncoding, "gzip")]
+        (LBS.fromStrict gzipBytes)
+
+  assertEqual "response is binary" True isBase64Encoded
+  assertEqual
+    "response round-trips through base64"
+    (Right gzipBytes)
+    (B64.decode (T.encodeUtf8 serialized))
+
+unit_IdentityResponseIsNotBase64Encoded :: Assertion
+unit_IdentityResponseIsNotBase64Encoded = do
+  ProxyResponse {body = ProxyBody {..}} <-
+    fromWaiResponse defaultOptions $
+      responseLBS
+        ok200
+        [(hContentType, "text/plain"), (hContentEncoding, "identity")]
+        "Hello, World!"
+
+  assertEqual "response is not binary" False isBase64Encoded
+  assertEqual "response is unmangled" "Hello, World!" serialized
+
+test_ContentEncodingDetection :: [TestTree]
+test_ContentEncodingDetection =
+  [ testGroup "base64-encoded" $
+      [ ("Gzip", ["Gzip"]),
+        ("br, gzip with whitespace", [" br , gzip"]),
+        ("identity, gzip", ["identity, gzip"]),
+        ("x-gzip", ["x-gzip"]),
+        ("repeated headers", ["identity", "br"])
+      ]
+        <&> \(title, values) -> testCase title $ do
+          ProxyResponse {body = ProxyBody {..}} <- mkResponse values
+          assertEqual "isBase64Encoded" True isBase64Encoded,
+    testGroup "not base64-encoded" $
+      [ ("empty value", [""]),
+        ("identity, mixed case", ["identity , Identity"]),
+        ("no header", [])
+      ]
+        <&> \(title, values) -> testCase title $ do
+          ProxyResponse {body = ProxyBody {..}} <- mkResponse values
+          assertEqual "isBase64Encoded" False isBase64Encoded
+  ]
+  where
+    mkResponse :: [ByteString] -> IO ProxyResponse
+    mkResponse values =
+      fromWaiResponse defaultOptions $
+        responseLBS
+          ok200
+          ((hContentType, "text/plain") : ((hContentEncoding,) <$> values))
+          "x"
diff --git a/wai-handler-hal.cabal b/wai-handler-hal.cabal
--- a/wai-handler-hal.cabal
+++ b/wai-handler-hal.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: wai-handler-hal
-version: 0.5.0.1
+version: 0.5.1.0
 synopsis: Wrap WAI applications to run on AWS Lambda
 description:
   This library provides a function 'Network.Wai.Handler.Hal.run' to
@@ -50,7 +50,7 @@
   build-depends:
     base >=4.12 && <4.23,
     base64-bytestring >=1.0.0.0 && <1.3,
-    bytestring >=0.10.8 && <0.13,
+    bytestring >=0.10.12 && <0.13,
     case-insensitive ^>=1.2.0.0,
     hal >=0.4.7 && <1.2,
     http-media ^>=0.8.1.1,
