5

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 ?

sinanspd
  • 2,589
  • 3
  • 19
  • 37

1 Answers1

0

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.

  • Sorry I am failing to see how this is the same problem. Where are you encoding a nested `Product` type here? The answer has a lot of undefined symbols, so I can't really tell. Is `code` a complex object? – sinanspd Sep 14 '21 at 16:20
  • For the problem, it doesn't matter, everything is around the Content-Type and how http4s defaults to `Content-Type: application/json`. Constructing the request as I did should solve the issue. In this way, the default header will be `x-www-form-urlencoded` – Juan Pablo Gómez Uribe Sep 15 '21 at 10:23
  • Ah. Gotcha. I will try it this weekend. I need to re-extract a minimal example from my use case. Will accept the answer once I confirm. Thank you – sinanspd Sep 16 '21 at 15:39