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 +10/−0
- README.md +5/−0
- src/Network/Wai/Handler/Hal.hs +52/−28
- test/Network/Wai/Handler/HalTest.hs +65/−3
- wai-handler-hal.cabal +2/−2
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,