graphql-engine/server/src-lib/Hasura/HTTP.hs
2022-07-11 08:05:40 +00:00

195 lines
9.3 KiB
Haskell

module Hasura.HTTP
( wreqOptions,
HttpException (..),
hdrsToText,
addDefaultHeaders,
defaultHeaders,
HttpResponse (..),
addHttpResponseHeaders,
getHTTPExceptionStatus,
serializeHTTPExceptionMessage,
serializeHTTPExceptionMessageForDebugging,
serializeServantClientErrorMessage,
serializeServantClientErrorMessageForDebugging,
)
where
import Control.Exception (Exception (..), fromException)
import Control.Lens hiding ((.=))
import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as KM
import Data.CaseInsensitive (original)
import Data.HashMap.Strict qualified as M
import Data.Text qualified as T
import Data.Text.Conversions (UTF8 (..), convertText)
import Data.Text.Encoding qualified as TE
import Data.Text.Encoding.Error qualified as TE
import Hasura.Prelude
import Hasura.Server.Utils (redactSensitiveHeader)
import Hasura.Server.Version (currentVersion)
import Network.HTTP.Client qualified as HTTP
import Network.HTTP.Client.Restricted qualified as Restricted
import Network.HTTP.Media qualified as HTTP
import Network.HTTP.Types qualified as HTTP
import Network.Wreq qualified as Wreq
import Servant.Client qualified as Servant
hdrsToText :: [HTTP.Header] -> [(Text, Text)]
hdrsToText hdrs =
[ (bsToTxt $ original hdrName, bsToTxt hdrVal)
| (hdrName, hdrVal) <- hdrs
]
wreqOptions :: HTTP.Manager -> [HTTP.Header] -> Wreq.Options
wreqOptions manager hdrs =
Wreq.defaults
& Wreq.headers .~ addDefaultHeaders hdrs
& Wreq.checkResponse ?~ (\_ _ -> return ())
& Wreq.manager .~ Right manager
-- Adds defaults headers overwriting any existing ones
addDefaultHeaders :: [HTTP.Header] -> [HTTP.Header]
addDefaultHeaders hdrs = defaultHeaders <> rmDefaultHeaders hdrs
where
rmDefaultHeaders = filter (not . isDefaultHeader)
isDefaultHeader :: HTTP.Header -> Bool
isDefaultHeader (hdrName, _) = hdrName `elem` map fst defaultHeaders
defaultHeaders :: [HTTP.Header]
defaultHeaders = [contentType, userAgent]
where
contentType = ("Content-Type", "application/json")
userAgent =
( "User-Agent",
"hasura-graphql-engine/" <> unUTF8 (convertText currentVersion)
)
newtype HttpException = HttpException
{unHttpException :: HTTP.HttpException}
deriving (Show)
getHTTPExceptionStatus :: HttpException -> Maybe Int
getHTTPExceptionStatus = \case
(HttpException (HTTP.HttpExceptionRequest _ httpExceptionContent)) ->
case httpExceptionContent of
HTTP.StatusCodeException response _ -> Just $ HTTP.statusCode $ HTTP.responseStatus response
HTTP.ProxyConnectException _ _ status -> Just $ HTTP.statusCode status
_ -> Nothing
(HttpException (HTTP.InvalidUrlException _ _)) -> Nothing
serializeHTTPExceptionMessage :: HttpException -> Text
serializeHTTPExceptionMessage (HttpException (HTTP.HttpExceptionRequest _ httpExceptionContent)) =
case httpExceptionContent of
HTTP.StatusCodeException _ _ -> "unexpected"
HTTP.TooManyRedirects _ -> "Too many redirects"
HTTP.OverlongHeaders -> "Overlong headers"
HTTP.ResponseTimeout -> "Response timeout"
HTTP.ConnectionTimeout -> "Connection timeout"
HTTP.ConnectionFailure _ -> "Connection failure"
HTTP.InvalidStatusLine _ -> "Invalid HTTP Status Line"
HTTP.InternalException err -> case fromException err of
Just (Restricted.ConnectionRestricted _ _) -> "Blocked connection to private IP address"
Nothing -> "Internal Exception"
HTTP.ProxyConnectException {} -> "Proxy connection exception"
HTTP.NoResponseDataReceived -> "No response data received"
HTTP.TlsNotSupported -> "TLS not supported"
HTTP.InvalidDestinationHost _ -> "Invalid destination host"
HTTP.InvalidHeader _ -> "Invalid Header"
HTTP.InvalidRequestHeader _ -> "Invalid Request Header"
HTTP.WrongRequestBodyStreamSize _ _ -> "Wrong request body stream size"
HTTP.ResponseBodyTooShort _ _ -> "Response body too short"
HTTP.InvalidChunkHeaders -> "Invalid chunk headers"
HTTP.IncompleteHeaders -> "Incomplete headers"
_ -> "unexpected"
serializeHTTPExceptionMessage (HttpException (HTTP.InvalidUrlException url reason)) = T.pack $ "URL: " <> url <> " is invalid because " <> reason
serializeHTTPExceptionMessageForDebugging :: HTTP.HttpException -> Text
serializeHTTPExceptionMessageForDebugging = \case
HTTP.HttpExceptionRequest _ err -> case err of
HTTP.StatusCodeException response _ -> "response status code indicated failure" <> (tshow . HTTP.statusCode $ HTTP.responseStatus response)
HTTP.TooManyRedirects redirects -> "too many redirects: " <> tshow (length redirects) <> " redirects"
HTTP.OverlongHeaders -> "overlong headers"
HTTP.ResponseTimeout -> "response timeout"
HTTP.ConnectionTimeout -> "connection timeout"
HTTP.ConnectionFailure exn -> "connection failure: " <> serializeExceptionForDebugging exn
HTTP.InvalidStatusLine statusLine -> "invalid status line: " <> fromUtf8 statusLine
HTTP.InvalidHeader header -> "invalid header: " <> fromUtf8 header
HTTP.InvalidRequestHeader requestHeader -> "invalid request header: " <> fromUtf8 requestHeader
HTTP.InternalException exn -> "internal error: " <> serializeExceptionForDebugging exn
HTTP.ProxyConnectException proxyHost port status -> "proxy connection to " <> fromUtf8 proxyHost <> ":" <> tshow port <> " returned response with status code that indicated failure: " <> tshow (HTTP.statusCode status)
HTTP.NoResponseDataReceived -> "no response data received"
HTTP.TlsNotSupported -> "TLS not supported"
HTTP.WrongRequestBodyStreamSize expected actual -> "wrong request body stream size. expected: " <> tshow expected <> ", actual: " <> tshow actual
HTTP.ResponseBodyTooShort expected actual -> "response body too short. expected: " <> tshow expected <> ", actual: " <> tshow actual
HTTP.InvalidChunkHeaders -> "invalid chunk headers"
HTTP.IncompleteHeaders -> "incomplete headers"
HTTP.InvalidDestinationHost host -> "invalid destination host: " <> fromUtf8 host
HTTP.HttpZlibException exn -> "HTTP zlib error: " <> serializeExceptionForDebugging exn
HTTP.InvalidProxyEnvironmentVariable name value -> "invalid proxy environment variable: " <> name <> "=" <> value
HTTP.ConnectionClosed -> "connection closed"
HTTP.InvalidProxySettings err' -> "invalid proxy settings: " <> err'
HTTP.InvalidUrlException url' reason -> "invalid url: " <> T.pack url' <> "; reason: " <> T.pack reason
where
fromUtf8 = TE.decodeUtf8With TE.lenientDecode
encodeHTTPRequestJSON :: HTTP.Request -> J.Value
encodeHTTPRequestJSON request =
J.Object $
KM.fromList
[ ("host", J.toJSON $ TE.decodeUtf8 $ HTTP.host request),
("port", J.toJSON $ HTTP.port request),
("secure", J.toJSON $ HTTP.secure request),
("requestHeaders", J.toJSON $ M.fromList $ hdrsToText $ map redactSensitiveHeader $ HTTP.requestHeaders request),
("path", J.toJSON $ TE.decodeUtf8 $ HTTP.path request),
("queryString", J.toJSON $ TE.decodeUtf8 $ HTTP.queryString request),
("method", J.toJSON $ TE.decodeUtf8 $ HTTP.method request),
("responseTimeout", J.String $ tshow $ HTTP.responseTimeout request)
]
instance J.ToJSON HttpException where
toJSON httpException =
case httpException of
(HttpException (HTTP.InvalidUrlException _ e)) ->
J.object
[ "type" J..= ("invalid_url" :: Text),
"message" J..= e
]
(HttpException (HTTP.HttpExceptionRequest req _)) ->
let statusMaybe = getHTTPExceptionStatus httpException
exceptionContent = serializeHTTPExceptionMessage httpException
reqJSON = encodeHTTPRequestJSON req
in J.object $
[ "type" J..= ("http_exception" :: Text),
"message" J..= exceptionContent,
"request" J..= reqJSON
]
<> maybe mempty (\status -> ["status" J..= status]) statusMaybe
data HttpResponse a = HttpResponse
{ _hrBody :: !a,
_hrHeaders :: !HTTP.ResponseHeaders
}
deriving (Functor, Foldable, Traversable)
addHttpResponseHeaders :: HTTP.ResponseHeaders -> HttpResponse a -> HttpResponse a
addHttpResponseHeaders newHeaders (HttpResponse b h) = HttpResponse b (newHeaders <> h)
serializeServantClientErrorMessage :: Servant.ClientError -> Text
serializeServantClientErrorMessage = \case
Servant.FailureResponse _ response -> "response status code indicated failure: " <> (tshow . HTTP.statusCode $ Servant.responseStatusCode response)
Servant.DecodeFailure decodeErrorText _ -> "unable to decode the response, " <> decodeErrorText
Servant.UnsupportedContentType mediaType _ -> "unsupported content type in response: " <> TE.decodeUtf8With TE.lenientDecode (HTTP.renderHeader mediaType)
Servant.InvalidContentTypeHeader _ -> "invalid content type in response"
Servant.ConnectionError _ -> "connection error"
serializeServantClientErrorMessageForDebugging :: Servant.ClientError -> Text
serializeServantClientErrorMessageForDebugging = \case
Servant.ConnectionError exn -> case fromException exn of
Just httpException -> serializeHTTPExceptionMessageForDebugging httpException
Nothing -> "error in the connection: " <> serializeExceptionForDebugging exn
other -> serializeServantClientErrorMessage other
serializeExceptionForDebugging :: Exception e => e -> Text
serializeExceptionForDebugging = T.pack . displayException