Http4s Client Encode Entity as x-www-form-urlencoded Recursively

1.2k views Asked by At

I have a request like the following

val request =
    Request[IO](
      method = POST,
      uri = Uri.uri("..."),
      headers = Headers(
        Authorization(BasicCredentials("...", "..."))
      )
    )
    .withEntity(PaymentIntentRequest2(2000, "usd"))

I am looking at the source code and it looks like the withEntity inherits the headers from the nested EntityDecoder so the code above defaults to Content-Type: application/json. Where as if I explicitly pass in UrlForm everything is fine.

Unfortunately the API I am hitting expected the data as x-www-form-urlencoded and given the complexity of the target API with all the different endpoints/requests I would like to find a way to encode the given case class as a form. What is the best way of doing that?

I have tried:

  1. Explicitly specifying the Content-Type but this doesn't work because the inherited type takes priority

  2. Building an implicit generic conversion from Product to UrlForm (extension method for now)

implicit class UrlFormEncode[+B <: Product](val u: B) {
    def asUrlForm: UrlForm =
      u.productElementNames
        .zip(u.productIterator)
        .foldLeft(UrlForm()) { (a, b) =>
          a.combine(UrlForm(b._1 -> b._2.toString))
        }
}

The problem here is UrlForm expects a string in both sides of the mapping. And if I just convert things with .toString it doesn't work because of nested typed for example:

ChargeRequest(Amount(refInt), EUR, source = Some(SourceId("...."))

Results in the following json which is not valid

{
  "currency": "EUR",
  "amount": "2000",
  "source": "Some(SourceId(....))",
  "customer": "None"
}

I tried asJson instead of toString but circe can not decide on the proper KeyEncoder

What is the right way of approaching this so the given Product is encoded down the stream ?

2

There are 2 answers

3
Juan Pablo Gómez Uribe On

I just faced the same issue and this is the way it worked for me.

From https://http4s.org/v0.20/client/

// This import will add the right `apply` to the POST.
import org.http4s.client.dsl.io._


val form = UrlForm(
      OAuthAttribute.Code        -> code,
      OAuthAttribute.RedirectUri -> callbackUri,
      OAuthAttribute.GrantType   -> "authorization_code"
    )

private def buildRequest(tokenUri: Uri, form: UrlForm, header: String): Request[IO] =
    POST(
      form,
      tokenUri,
      Header.Raw(CIString("Authorization"), header),
      Header.Raw(CIString("Content-Type"), "application/x-www-form-urlencoded"),
      Header.Raw(CIString("Accept"), "application/json")
    )

And that's it. For some strange reason using .withHeaders didn't work for me, seems like they are overridden or so.

0
hlopezvg On

In my particular case, and updating a bit the base information provided from @Juan Pablo Gómez Uribe I was able to request a token by using:

val request = POST(
    UrlForm(
      "client_id" -> "CLIENT_ID",
      "grant_type" -> "ANY_TYPE",
      "scope" -> "string1 string2 string3",
      "client_secret" -> "CLIENT_SECRET"
    ),
    uri"https://urlexample.com/oauth2/token",
    Headers(
       Header.Raw(CIString("Content-Type"), "application/x-www-form-urlencoded"),
       Header.Raw(CIString("Accept"), "*/*"),
       Header.Raw(CIString("Accept-Encoding"), "gzip, deflate, br"),
       Header.Raw(CIString("Connection"), "keep-alive")
    )
  )