packages feed

wai-handler-hal 0.5.0.1 → 0.5.1.0

raw patch · 5 files changed

+134/−33 lines, 5 filesdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -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
README.md view
@@ -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
src/Network/Wai/Handler/Hal.hs view
@@ -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
test/Network/Wai/Handler/HalTest.hs view
@@ -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"
wai-handler-hal.cabal view
@@ -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,