Search code examples
scalacallbackiocats-effect

IO.async callback problem with cats.effect in Scala


Im trying to rewrite a httpclient through the java11 HttpClient in Scala

Here is my code:

import cats.effect._
import java.net.http._
import java.net.http.HttpResponse._
import java.net.http.HttpClient._

trait HttpClients[F[_]] {
  def send(req: HttpRequest)(implicit F: Async[F]): F[HttpResponse[_]]
}

object HttpClients {
  val client: HttpClient = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).build()
  def newClient[F[_] : Async](): HttpClients[F] = new HttpClients[F] {
    override def send(req: HttpRequest)(implicit F: Async[F]): F[HttpResponse[_]] = F.async { cb =>
      val resp = client.sendAsync(req, BodyHandlers.ofString())
      val s = resp.handle((res: HttpResponse[String], err: Throwable) => {
        if (err == null)
          cb(Right(res))
        else
          cb(Left(err))
      })
      s // TODO ?
      // Type missmatch
      // Required: F[Option[F[Unit]]]
      // Found:    Unit
    }
  }
}

the handle callback from this

I guess the error comes from here, but I don't know how to write next.

Then I make some change:

  def newClient[F[_] : Async](): HttpClients[F] = new HttpClients[F] {
    override def send(req: HttpRequest)(implicit F: Async[F]): F[HttpResponse[_]] = F.async[HttpResponse[_]] { cb =>
      val s = Sync[F](F: Async[F]).delay {
        val resp = client.sendAsync(req, BodyHandlers.ofString())
        resp.handle((res: HttpResponse[String], err: Throwable) => {
          if (err == null)
            cb(Right(res))
          else
            cb(Left(err))
        }).join()
      }
      F.delay(s.some)
    }
  }

This time, there is no error, but I don't know how to get the response's body

Thanks for your reply!


Solution

  • @OlegPyzhcov already provided insight in case you are using CE3, this answer is using CE2 in case that is what you wanted.

    The first version of the code was correct, here is a full running example using Ammonite with some style improvements and ensuring a new client is created for each call and evaluation of newClient

    // scala 2.13.5
    
    import $ivy.`org.typelevel::cats-effect:2.5.0`
    
    import cats.effect.{Async, IO}
    import cats.syntax.all._
    import java.net.URI
    import java.net.http.{HttpClient, HttpRequest, HttpResponse}
    
    trait HttpClients[F[_]] {
      def send(req: HttpRequest): F[HttpResponse[String]]
    }
    
    object HttpClients {
      def newClient[F[_]](implicit F: Async[F]): F[HttpClients[F]] =
        F.delay {
          HttpClient
            .newBuilder
            .followRedirects(HttpClient.Redirect.ALWAYS)
            .build()
        } map { client =>
          new HttpClients[F] {
            override def send(req: HttpRequest): F[HttpResponse[String]] =
              F.async { cb =>
                client.sendAsync(req, HttpResponse.BodyHandlers.ofString).handle {
                  (res: HttpResponse[String], err: Throwable) =>
                    if (err == null) cb(Right(res))
                    else cb(Left(err))
                }
              }
          }
        }
    }
    
    object Main {
      private val request =
        HttpRequest
          .newBuilder
          .GET
          .uri(URI.create("https://stackoverflow.com/questions/tagged/scala?tab=Newest"))
          .build()
    
      private val program = for {
        _ <- IO.delay(println("Hello, World!"))
        client <- HttpClients.newClient[IO]
        response <- client.send(request)
        _ <- IO.delay(println(response))
        _ <- IO.delay(println(response.body))
      } yield ()
    
      def run(): Unit = {
        program.unsafeRunSync()
      }
    }
    
    @main
    def main(): Unit = {
      Main.run()
    }