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:
Profpatsch 2024-08-04 11:09:26 +02:00
parent 59056cf705
commit 8908fd18ca
3 changed files with 70 additions and 62 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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