Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more detailed error messages on Context failure #84

Merged
merged 3 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions src/AWS/Lambda/RuntimeClient/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ module AWS.Lambda.RuntimeClient.Internal (
import AWS.Lambda.Context (LambdaContext)
import AWS.Lambda.Internal (DynamicContext (..), StaticContext,
mkContext)
import Data.Aeson (Value, decode)
import Data.Aeson (Value, eitherDecode)
import Data.Aeson.Types (FromJSON)
import Data.Bifunctor (first)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Internal as BSI
import qualified Data.ByteString.Lazy as BSW
import Data.CaseInsensitive (original)
import Data.Semigroup ((<>))
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Network.HTTP.Client (Response, responseBody,
Expand All @@ -29,36 +32,27 @@ import Network.HTTP.Types (HeaderName)
eventResponseToNextData :: StaticContext -> Response Value -> (BS.ByteString, Value, Either String LambdaContext)
eventResponseToNextData staticContext nextRes =
-- If we got an event but our requestId is invalid/missing, there's no hope of meaningful recovery
let reqIdBS = head $ getResponseHeader "Lambda-Runtime-Aws-Request-Id" nextRes
let
reqIdBS = head $ getResponseHeader "Lambda-Runtime-Aws-Request-Id" nextRes

mTraceId = fmap decodeUtf8 $ exactlyOneHeader $ getResponseHeader "Lambda-Runtime-Trace-Id" nextRes
mFunctionArn = fmap decodeUtf8 $ exactlyOneHeader $ getResponseHeader "Lambda-Runtime-Invoked-Function-Arn" nextRes
mDeadline = do
header <- exactlyOneHeader (getResponseHeader "Lambda-Runtime-Deadline-Ms" nextRes)
milliseconds :: Double <- readMaybe $ BSC.unpack header
return $ posixSecondsToUTCTime $ realToFrac $ milliseconds / 1000
eCtx = first ("Runtime Error: Unable to decode Context from event response.\n" <>) $ do
traceId <- fmap decodeUtf8 $ exactlyOneHeader "Lambda-Runtime-Trace-Id" nextRes
functionArn <- fmap decodeUtf8 $ exactlyOneHeader "Lambda-Runtime-Invoked-Function-Arn" nextRes
deadlineHeader <- exactlyOneHeader "Lambda-Runtime-Deadline-Ms" nextRes
milliseconds :: Double <- maybeToEither "Could not parse deadline" $ readMaybe $ BSC.unpack deadlineHeader
let deadline = posixSecondsToUTCTime $ realToFrac $ milliseconds / 1000

mClientContext = decodeOptionalHeader $ getResponseHeader "Lambda-Runtime-Client-Context" nextRes
mIdentity = decodeOptionalHeader $ getResponseHeader "Lambda-Runtime-Cognito-Identity" nextRes
clientContext <- decodeOptionalHeader "Lambda-Runtime-Client-Context" nextRes
identity <- decodeOptionalHeader "Lambda-Runtime-Cognito-Identity" nextRes

-- Build out the Dynamic portion of the Lambda Context
eDynCtx =
maybeToEither "Runtime Error: Unable to decode Context from event response."
-- Build the Dynamic Context, collapsing individual Maybes into a single Maybe
$ DynamicContext (decodeUtf8 reqIdBS)
<$> mFunctionArn
<*> mTraceId
<*> mDeadline
<*> mClientContext
<*> mIdentity
let dynCtx = DynamicContext (decodeUtf8 reqIdBS) functionArn traceId deadline clientContext identity

-- combine our StaticContext and possible DynamicContext into a LambdaContext
eCtx = fmap (mkContext staticContext) eDynCtx

event = getResponseBody nextRes
return (mkContext staticContext dynCtx)

-- Return the interesting components
in (reqIdBS, event, eCtx)
in (reqIdBS, getResponseBody nextRes, eCtx)


-- Helpers (mostly) for Headers
Expand All @@ -69,9 +63,16 @@ getResponseBody = responseBody
getResponseHeader :: HeaderName -> Response a -> [BS.ByteString]
getResponseHeader headerName = fmap snd . filter ((==) headerName . fst) . responseHeaders

exactlyOneHeader :: [a] -> Maybe a
exactlyOneHeader [a] = Just a
exactlyOneHeader _ = Nothing
headerNameToString :: HeaderName -> String
headerNameToString = fmap BSI.w2c . BS.unpack . original

exactlyOneHeader :: HeaderName -> Response Value -> Either String BS.ByteString
exactlyOneHeader name res =
let nameStr = headerNameToString name
in case getResponseHeader name res of
[a] -> Right a
[] -> Left ("Missing response header " <> nameStr)
_ -> Left ("Too many values for header " <> nameStr)

maybeToEither :: b -> Maybe a -> Either b a
maybeToEither b ma = case ma of
Expand All @@ -85,16 +86,17 @@ readMaybe s = case reads s of
_ -> Nothing

-- TODO: There must be a better way to do this
decodeHeaderValue :: FromJSON a => BSC.ByteString -> Maybe a
decodeHeaderValue = decode . BSW.pack . fmap BSI.c2w . BSC.unpack
decodeHeaderValue :: FromJSON a => BSC.ByteString -> Either String a
decodeHeaderValue = eitherDecode . BSW.pack . fmap BSI.c2w . BSC.unpack

-- An empty array means we successfully decoded, but nothing was there
-- If we have exactly one element, our outer maybe signals successful decode,
-- and our inner maybe signals that there was content sent
-- If we had more than one header value, the event was invalid
decodeOptionalHeader :: FromJSON a => [BSC.ByteString] -> Maybe (Maybe a)
decodeOptionalHeader header =
case header of
[] -> Just Nothing
[x] -> fmap Just $ decodeHeaderValue x
_ -> Nothing
decodeOptionalHeader :: FromJSON a => HeaderName -> Response Value -> Either String (Maybe a)
decodeOptionalHeader name res =
let nameStr = headerNameToString name
in case getResponseHeader name res of
[] -> Right Nothing
[x] -> first (\e -> "Could not JSON decode header " <> nameStr <> ": " <> e) $ fmap Just $ decodeHeaderValue x
_ -> Left ("Too many values for header " <> nameStr)
40 changes: 26 additions & 14 deletions test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Data.Semigroup ((<>))
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Network.HTTP.Client.Internal (Response (..))
import Network.HTTP.Types (Header)
import Test.Hspec (describe, it, shouldBe)
import Test.Hspec (describe, it, shouldBe,
shouldStartWith)
import Test.Hspec.Runner (hspec)

main :: IO ()
Expand Down Expand Up @@ -79,8 +80,9 @@ main =
("Lambda-Runtime-Client-Context", "{}") : basicValidHeaders
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
fmap clientContext context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
let msg = either id (const (error "Was able to parse a context that should have failed!")) context
msg `shouldStartWith`
"Runtime Error: Unable to decode Context from event response.\nCould not JSON decode header Lambda-Runtime-Client-Context: "
it
"fails to construct the Context if there are two client context headers" $ do
let headers =
Expand All @@ -91,7 +93,8 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
fmap clientContext context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nToo many values for header Lambda-Runtime-Client-Context")
it "has identity if it was provided" $ do
let headers =
( "Lambda-Runtime-Cognito-Identity"
Expand All @@ -112,8 +115,9 @@ main =
("Lambda-Runtime-Cognito-Identity", "{}") : basicValidHeaders
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
fmap clientContext context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
let msg = either id (const (error "Was able to parse a context that should have failed!")) context
msg `shouldStartWith`
"Runtime Error: Unable to decode Context from event response.\nCould not JSON decode header Lambda-Runtime-Cognito-Identity: "
it
"fails to construct the Context if there are two cognito identity headers" $ do
let headers =
Expand All @@ -124,7 +128,8 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
fmap clientContext context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nToo many values for header Lambda-Runtime-Cognito-Identity")
it "fails to create the context if trace Id is not provided" $ do
let headers =
[ ("Lambda-Runtime-Aws-Request-Id", "abc")
Expand All @@ -134,13 +139,15 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nMissing response header Lambda-Runtime-Trace-Id")
it "fails to create the context if trace id has multiple values" $ do
let headers = ("Lambda-Runtime-Trace-Id", "123") : basicValidHeaders
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nToo many values for header Lambda-Runtime-Trace-Id")
it "fails to create the context if function ARN is not provided" $ do
let headers =
[ ("Lambda-Runtime-Aws-Request-Id", "abc")
Expand All @@ -150,14 +157,16 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nMissing response header Lambda-Runtime-Invoked-Function-Arn")
it "fails to create the context if function ARN has multiple values" $ do
let headers =
("Lambda-Runtime-Invoked-Function-Arn", "arn") : basicValidHeaders
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nToo many values for header Lambda-Runtime-Invoked-Function-Arn")
it "fails to create the context if a deadline is not provided" $ do
let headers =
[ ("Lambda-Runtime-Aws-Request-Id", "abc")
Expand All @@ -167,14 +176,16 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nMissing response header Lambda-Runtime-Deadline-Ms")
it "fails to create the context if the deadline has multiple values" $ do
let headers =
("Lambda-Runtime-Deadline-Ms", "12332000") : basicValidHeaders
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nToo many values for header Lambda-Runtime-Deadline-Ms")
it "fails to create the context if the deadline is not a valid timestamp" $ do
let headers =
[ ("Lambda-Runtime-Aws-Request-Id", "abc")
Expand All @@ -185,7 +196,8 @@ main =
let (_, _, context) =
eventResponseToNextData staticContext (minJsonResponse headers)
context `shouldBe`
(Left "Runtime Error: Unable to decode Context from event response.")
(Left
"Runtime Error: Unable to decode Context from event response.\nCould not parse deadline")

minResponse :: [Header] -> a -> Response a
minResponse headers body =
Expand Down