refactor(users/Profpatsch/whatcd-resolver): start moving http stuff
There’s a bunch of duplication in how http client things are done, let’s move that all to a single module. Change-Id: Ic08c9bce49d562e4fa640a5bdfc15973a28a7bcb Reviewed-on: https://cl.tvl.fyi/c/depot/+/12135 Reviewed-by: Profpatsch <mail@profpatsch.de> Tested-by: BuildkiteCI
This commit is contained in:
parent
59056cf705
commit
8908fd18ca
3 changed files with 70 additions and 62 deletions
|
@ -4,27 +4,35 @@ module Http
|
||||||
( doRequestJson,
|
( doRequestJson,
|
||||||
RequestOptions (..),
|
RequestOptions (..),
|
||||||
mkRequestOptions,
|
mkRequestOptions,
|
||||||
setRequestMethod,
|
httpJson,
|
||||||
setRequestBodyLBS,
|
Http.setRequestMethod,
|
||||||
setRequestHeader,
|
Http.setRequestBodyLBS,
|
||||||
getResponseStatus,
|
Http.setRequestHeader,
|
||||||
getResponseHeader,
|
Http.getResponseStatus,
|
||||||
getResponseBody,
|
Http.getResponseHeader,
|
||||||
|
Http.getResponseBody,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import AppT
|
import AppT
|
||||||
|
import Data.Aeson.BetterErrors qualified as Json
|
||||||
import Data.CaseInsensitive (CI (original))
|
import Data.CaseInsensitive (CI (original))
|
||||||
import Data.Char qualified as Char
|
import Data.Char qualified as Char
|
||||||
|
import Data.Error.Tree
|
||||||
import Data.List qualified as List
|
import Data.List qualified as List
|
||||||
import Data.Text qualified as Text
|
import Data.Text qualified as Text
|
||||||
import Data.Text.Punycode qualified as Punycode
|
import Data.Text.Punycode qualified as Punycode
|
||||||
|
import Json qualified
|
||||||
import Json.Enc qualified as Enc
|
import Json.Enc qualified as Enc
|
||||||
|
import Label
|
||||||
import MyPrelude
|
import MyPrelude
|
||||||
import Network.HTTP.Client
|
import Network.HTTP.Client
|
||||||
import Network.HTTP.Simple
|
import Network.HTTP.Client qualified as Http
|
||||||
|
import Network.HTTP.Simple qualified as Http
|
||||||
import Network.HTTP.Types.Status (Status (..))
|
import Network.HTTP.Types.Status (Status (..))
|
||||||
|
import Network.Wai.Parse qualified as Wai
|
||||||
import Optional
|
import Optional
|
||||||
|
import Pretty
|
||||||
import Prelude hiding (span)
|
import Prelude hiding (span)
|
||||||
|
|
||||||
data RequestOptions = RequestOptions
|
data RequestOptions = RequestOptions
|
||||||
|
@ -32,7 +40,7 @@ data RequestOptions = RequestOptions
|
||||||
host :: Text,
|
host :: Text,
|
||||||
port :: Optional Int,
|
port :: Optional Int,
|
||||||
path :: Optional [Text],
|
path :: Optional [Text],
|
||||||
headers :: Optional [Header],
|
headers :: Optional [Http.Header],
|
||||||
usePlainHttp :: Optional Bool
|
usePlainHttp :: Optional Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +55,47 @@ mkRequestOptions opts =
|
||||||
usePlainHttp = defaults
|
usePlainHttp = defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpJson ::
|
||||||
|
( MonadThrow m,
|
||||||
|
MonadOtel m
|
||||||
|
) =>
|
||||||
|
(Optional (Label "contentType" ByteString)) ->
|
||||||
|
Json.Parse ErrorTree b ->
|
||||||
|
Http.Request ->
|
||||||
|
m b
|
||||||
|
httpJson opts parser req = inSpan' "HTTP Request (JSON)" $ \span -> do
|
||||||
|
let opts' = opts.withDefault (label @"contentType" "application/json")
|
||||||
|
Http.httpBS req
|
||||||
|
>>= assertM
|
||||||
|
span
|
||||||
|
( \resp -> do
|
||||||
|
let statusCode = resp & Http.responseStatus & (.statusCode)
|
||||||
|
contentType =
|
||||||
|
resp
|
||||||
|
& Http.responseHeaders
|
||||||
|
& List.lookup "content-type"
|
||||||
|
<&> Wai.parseContentType
|
||||||
|
<&> (\(ct, _mimeAttributes) -> ct)
|
||||||
|
if
|
||||||
|
| statusCode == 200,
|
||||||
|
Just ct <- contentType,
|
||||||
|
ct == opts'.contentType ->
|
||||||
|
Right $ (resp & Http.responseBody)
|
||||||
|
| statusCode == 200,
|
||||||
|
Just otherType <- contentType ->
|
||||||
|
Left [fmt|Server returned a non-json body, with content-type "{otherType}"|]
|
||||||
|
| statusCode == 200,
|
||||||
|
Nothing <- contentType ->
|
||||||
|
Left [fmt|Server returned a body with unspecified content type|]
|
||||||
|
| code <- statusCode -> Left [fmt|Server returned an non-200 error code, code {code}: {resp & showPretty}|]
|
||||||
|
)
|
||||||
|
>>= assertM
|
||||||
|
span
|
||||||
|
( \body ->
|
||||||
|
Json.parseStrict parser body
|
||||||
|
& first (Json.parseErrorTree "could not parse redacted response")
|
||||||
|
)
|
||||||
|
|
||||||
doRequestJson ::
|
doRequestJson ::
|
||||||
(MonadOtel m) =>
|
(MonadOtel m) =>
|
||||||
RequestOptions ->
|
RequestOptions ->
|
||||||
|
@ -56,16 +105,16 @@ doRequestJson opts val = inSpan' "HTTP Request (JSON)" $ \span -> do
|
||||||
addAttribute span "request.xh" (requestToXhCommandLine opts val)
|
addAttribute span "request.xh" (requestToXhCommandLine opts val)
|
||||||
resp <-
|
resp <-
|
||||||
defaultRequest {secure = not (opts & optsUsePlainHttp)}
|
defaultRequest {secure = not (opts & optsUsePlainHttp)}
|
||||||
& setRequestHost (opts & optsHost)
|
& Http.setRequestHost (opts & optsHost)
|
||||||
& setRequestPort (opts & optsPort)
|
& Http.setRequestPort (opts & optsPort)
|
||||||
-- TODO: is this automatically escaped by the library?
|
-- TODO: is this automatically escaped by the library?
|
||||||
& setRequestPath (opts & optsPath)
|
& Http.setRequestPath (opts & optsPath)
|
||||||
& setRequestHeaders (opts & optsHeaders)
|
& Http.setRequestHeaders (opts & optsHeaders)
|
||||||
& setRequestMethod opts.method
|
& Http.setRequestMethod opts.method
|
||||||
& setRequestBodyLBS (Enc.encToBytesUtf8Lazy val)
|
& Http.setRequestBodyLBS (Enc.encToBytesUtf8Lazy val)
|
||||||
& httpBS
|
& Http.httpBS
|
||||||
let code = resp & getResponseStatus & (.statusCode)
|
let code = resp & Http.getResponseStatus & (.statusCode)
|
||||||
let msg = resp & getResponseStatus & (.statusMessage) & bytesToTextUtf8Lenient
|
let msg = resp & Http.getResponseStatus & (.statusMessage) & bytesToTextUtf8Lenient
|
||||||
addAttribute
|
addAttribute
|
||||||
span
|
span
|
||||||
"request.response.status"
|
"request.response.status"
|
||||||
|
@ -87,7 +136,7 @@ optsPort opts = opts.port.withDefault (if opts & optsUsePlainHttp then 80 else 4
|
||||||
optsPath :: RequestOptions -> ByteString
|
optsPath :: RequestOptions -> ByteString
|
||||||
optsPath opts = opts.path.withDefault [] & Text.intercalate "/" & ("/" <>) & textToBytesUtf8
|
optsPath opts = opts.path.withDefault [] & Text.intercalate "/" & ("/" <>) & textToBytesUtf8
|
||||||
|
|
||||||
optsHeaders :: RequestOptions -> [Header]
|
optsHeaders :: RequestOptions -> [Http.Header]
|
||||||
optsHeaders opts = opts.headers.withDefault []
|
optsHeaders opts = opts.headers.withDefault []
|
||||||
|
|
||||||
-- | Create a string that can be pasted on the command line to invoke the same HTTP request via the `xh` tool (curl but nicer syntax)
|
-- | Create a string that can be pasted on the command line to invoke the same HTTP request via the `xh` tool (curl but nicer syntax)
|
||||||
|
|
|
@ -11,16 +11,15 @@ import Data.Map.Strict qualified as Map
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import Data.Set qualified as Set
|
import Data.Set qualified as Set
|
||||||
import Html qualified
|
import Html qualified
|
||||||
|
import Http
|
||||||
import IHP.HSX.QQ (hsx)
|
import IHP.HSX.QQ (hsx)
|
||||||
import Json qualified
|
import Json qualified
|
||||||
import Label
|
import Label
|
||||||
import MyPrelude
|
import MyPrelude
|
||||||
import Network.HTTP.Client.Conduit qualified as Http
|
import Network.HTTP.Client.Conduit qualified as Http
|
||||||
import Network.HTTP.Simple qualified as Http
|
|
||||||
import Network.HTTP.Types.URI qualified as Url
|
import Network.HTTP.Types.URI qualified as Url
|
||||||
import Network.URI (URI)
|
import Network.URI (URI)
|
||||||
import Optional
|
import Optional
|
||||||
import Redacted
|
|
||||||
import Text.Blaze.Html (Html)
|
import Text.Blaze.Html (Html)
|
||||||
import Prelude hiding (span)
|
import Prelude hiding (span)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Database.PostgreSQL.Simple (Binary (Binary), Only (..))
|
||||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||||
import Database.PostgreSQL.Simple.Types (PGArray (PGArray))
|
import Database.PostgreSQL.Simple.Types (PGArray (PGArray))
|
||||||
import FieldParser qualified as Field
|
import FieldParser qualified as Field
|
||||||
|
import Http qualified
|
||||||
import Json qualified
|
import Json qualified
|
||||||
import Label
|
import Label
|
||||||
import MyPrelude
|
import MyPrelude
|
||||||
|
@ -522,47 +523,6 @@ httpTorrent span req =
|
||||||
| code <- statusCode -> Left [fmt|Redacted returned an non-200 error code, code {code}: {resp & showPretty}|]
|
| code <- statusCode -> Left [fmt|Redacted returned an non-200 error code, code {code}: {resp & showPretty}|]
|
||||||
)
|
)
|
||||||
|
|
||||||
httpJson ::
|
|
||||||
( MonadThrow m,
|
|
||||||
MonadOtel m
|
|
||||||
) =>
|
|
||||||
(Optional (Label "contentType" ByteString)) ->
|
|
||||||
Json.Parse ErrorTree b ->
|
|
||||||
Http.Request ->
|
|
||||||
m b
|
|
||||||
httpJson opts parser req = inSpan' "HTTP Request (JSON)" $ \span -> do
|
|
||||||
let opts' = opts.withDefault (label @"contentType" "application/json")
|
|
||||||
Http.httpBS req
|
|
||||||
>>= assertM
|
|
||||||
span
|
|
||||||
( \resp -> do
|
|
||||||
let statusCode = resp & Http.responseStatus & (.statusCode)
|
|
||||||
contentType =
|
|
||||||
resp
|
|
||||||
& Http.responseHeaders
|
|
||||||
& List.lookup "content-type"
|
|
||||||
<&> Wai.parseContentType
|
|
||||||
<&> (\(ct, _mimeAttributes) -> ct)
|
|
||||||
if
|
|
||||||
| statusCode == 200,
|
|
||||||
Just ct <- contentType,
|
|
||||||
ct == opts'.contentType ->
|
|
||||||
Right $ (resp & Http.responseBody)
|
|
||||||
| statusCode == 200,
|
|
||||||
Just otherType <- contentType ->
|
|
||||||
Left [fmt|Server returned a non-json body, with content-type "{otherType}"|]
|
|
||||||
| statusCode == 200,
|
|
||||||
Nothing <- contentType ->
|
|
||||||
Left [fmt|Server returned a body with unspecified content type|]
|
|
||||||
| code <- statusCode -> Left [fmt|Server returned an non-200 error code, code {code}: {resp & showPretty}|]
|
|
||||||
)
|
|
||||||
>>= assertM
|
|
||||||
span
|
|
||||||
( \body ->
|
|
||||||
Json.parseStrict parser body
|
|
||||||
& first (Json.parseErrorTree "could not parse redacted response")
|
|
||||||
)
|
|
||||||
|
|
||||||
redactedApiRequestJson ::
|
redactedApiRequestJson ::
|
||||||
( MonadThrow m,
|
( MonadThrow m,
|
||||||
HasField "action" p ByteString,
|
HasField "action" p ByteString,
|
||||||
|
@ -576,4 +536,4 @@ redactedApiRequestJson ::
|
||||||
redactedApiRequestJson dat parser =
|
redactedApiRequestJson dat parser =
|
||||||
do
|
do
|
||||||
mkRedactedApiRequest dat
|
mkRedactedApiRequest dat
|
||||||
>>= httpJson defaults parser
|
>>= Http.httpJson defaults parser
|
||||||
|
|
Loading…
Reference in a new issue