1. Introduction

http4s is a typed and functional HTTP library for Scala, with support for streaming. In this tutorial, we’ll go through an introduction to http4s, focusing on the basic topics. In particular, we’ll see how to create a simple HTTP server using http4s DSL as well as how to make HTTP calls using the http4s client.

2. http4s at a Glance

http4s is built upon a number of other libraries, such as cats (with which it shares a few core data types), cats-effect (to write effectful code), and FS2 (for streaming support). Let’s start by adding the dependencies to our build.sbt:

val http4sVersion = "0.23.18"
val http4sBlaze = "0.23.13"

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-blaze-server" % http4sBlaze,
  "org.http4s" %% "http4s-blaze-client" % http4sBlaze
)

3. http4s DSL for Endpoint Definition

http4s is based on the concepts of Request and Response. The library defines a server as a set of routes that contains functions that turn Requests into Responses, using the type HttpRoutes[F]. HttpRoutes[F] is an alias for Kleisli[OptionT[F, *], Request, Response], which is a fairly complex type. In a few words, Kleisli[OptionT[F, *], Request, Response] represents a wrapper around Request => F[Option[Response]], where F represents an effectful operation. We use Option since not every Request leads to a Response. We need F because generating a response might involve side effects, such as accessing a database or calling an external service. Hence, HttpRoutes[F] represents a function from a Request to an optional Response, possibly involving side effects. http4s DSL is used to define a route by pattern matching on the request. Let’s see how to define a simple endpoint that returns the length of a string passed in as a parameter:

implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global

val route: HttpRoutes[IO] = HttpRoutes.of[IO] {
  case GET -> Root / "length" / str => Ok(str.length.toString)
}

*The example above defines a single route responding to the endpoint length/{str}, where {str} represents a path parameter.* Using http4s DSL, we first specify that we want to respond to a GET request, then the endpoint, and then the behavior. Root indicates the root path configured by the server. For example, if we configure our server to use / as a base path, the endpoint will respond at /length. On the other hand, if our server used /api as a base path, we could reach the endpoint at /api/length. Path parameters are mapped to String by default in http4s. Therefore, str automatically assumes String as a type. To define the route, we also have to define a runtime, which is provided by IORuntime. Similarly, we use IO as an effectful operation. This means our route will output a value (the response) as a side effect.

4. Running a Server

After defining a route, we can set up a server to expose our endpoint and be able to call it. http4s supports multiple server backgrounds. For our examples, we’ll use blaze, the native server. Let’s setup a simple server:

object SimpleServer extends IOApp {
  val route: HttpRoutes[IO] = HttpRoutes.of[IO] {
    case GET -> Root / "length" / str => Ok(str.length.toString)
  }

  val app: Kleisli[IO, Request[IO], Response[IO]] = Router(
    "/" -> route
  ).orNotFound

  override def run(args: List[String]): IO[ExitCode] =
    BlazeServerBuilder[IO]
      .bindHttp(8080, "localhost")
      .withHttpApp(app)
      .resource
      .useForever
      .as(ExitCode.Success)
}

In this case, we bound the route to a base endpoint, “*/*“, when creating app. This will publish our route to the /length endpoint. We also say, by calling .orNotFound, that a 404 response should be returned if the request sent by the user does not match any endpoints we defined. The first step to running the server is to extend IOApp. This gives us the IO runtime without having to import it as before. It also handles the starting and terminating of the server. This way, the port bound to the server will get released as soon as the application gets closed. IOApp defines the main method for us and lets us specify the behavior of our server in the run method. Here we’re creating a BlazeServerBuilder. Next, we tell http4s the hostname and the port to bind the server to, localhost and 8080, respectively. Then, we select the “backend”, defined by app. The next steps are needed by http4s to start and terminate the server. BlazeServerBuilder[IO].resource returns the actual Server. This is where the effectful nature of http4s comes into play, as starting a server is a side effect. The simple call to resource will not allocate the server. For that, we have to call useForever, which binds port 8080 to our application on localhost. useForever tells http4s to run our server until we close the application explicitly. Lastly, we specify the exit code of the application.

5. http4s HTTP Client

After creating our simple HTTP server, let’s see how to make HTTP calls to it:

import cats.effect.unsafe.implicits.global

object SimpleClient extends IOApp {
  def callEffect(client: Client[IO], str: String): IO[String] =
    client.expect[String](uri"http://localhost:8080/length/" / str)

  override def run(args: List[String]): IO[ExitCode] =
    BlazeClientBuilder[IO].resource
      .use { client =>
        println(callEffect(client, "Baeldung").unsafeRunSync())
        IO.unit
      }
      .as(ExitCode.Success)
}

As before, we extend IOApp and use blaze as a background, and we create a BlazeClientBuilder. After retrieving the resource, we call use, which allocates the actual client and supplies it to a lambda function. The client is released as soon as we complete the effect. In this case, we do so by calling IO.unit. callEffect is where we describe and make the HTTP call to the simple server defined above. It inputs the HTTP client, as supplied by use, and a String to be passed as an argument to the HTTP call. In callEffect, we create a Uri object. In this case, we’re also making use of a dedicated DSL, allowing us to pass str as a path parameter. Next, we call expect on the client, to execute a GET request. The type of the response will be a String, but we could map the response into something else just by using map. For example, if we wanted to turn the response into an Int we could call client.expect[String](uri”http://localhost:8080/length/” / str).map(_.toInt). callEffect returns an IO effect of type String. *In order to get the actual response, we’ll need to run this effect via unsafeRunSync(), which needs an implicit runtime.* In this case, we import cats.effect.unsafe.implicits.global. unsafeRunSync produces a result by running the encapsulated effect as an impure side effect. Impure, in this context, means that the call will block the current thread until the value is ready. Additionally, running the effect this way might throw an exception. Ideally, this method is to be called once, at the very end of the program.

6. Conclusion

In this article, we saw a very brief introduction to http4s, which is a pretty complex library. We took a look at the definition of an endpoint via a DSL as well as at how to describe and perform HTTP calls. As usual, you can find the code over on GitHub.


» 下一篇: Scalatra 指南