Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http4s - migrate from Blaze to Ember #4185

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ lazy val clientTestServer = (projectMatrix in file("client/testserver"))
publish / skip := true,
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % Versions.http4s,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer,
"org.http4s" %% "http4s-ember-server" % Versions.http4s,
"org.http4s" %% "http4s-circe" % Versions.http4s,
logback
),
Expand Down Expand Up @@ -534,7 +534,7 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests"))
"io.github.classgraph" % "classgraph" % "4.8.179",
"org.http4s" %% "http4s-core" % Versions.http4s,
"org.http4s" %% "http4s-dsl" % Versions.http4s,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer,
"org.http4s" %% "http4s-ember-server" % Versions.http4s,
"org.typelevel" %%% "cats-effect" % Versions.catsEffect,
logback
),
Expand Down Expand Up @@ -1165,7 +1165,7 @@ lazy val swaggerUiBundle: ProjectMatrix = (projectMatrix in file("docs/swagger-u
name := "tapir-swagger-ui-bundle",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % Versions.sttpApispec,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer % Test,
"org.http4s" %% "http4s-ember-server" % Versions.http4s % Test,
scalaTest.value % Test
)
)
Expand All @@ -1191,7 +1191,7 @@ lazy val redocBundle: ProjectMatrix = (projectMatrix in file("docs/redoc-bundle"
name := "tapir-redoc-bundle",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % Versions.sttpApispec,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer % Test,
"org.http4s" %% "http4s-ember-server" % Versions.http4s % Test,
scalaTest.value % Test
)
)
Expand Down Expand Up @@ -1326,7 +1326,7 @@ lazy val http4sServer: ProjectMatrix = (projectMatrix in file("server/http4s-ser
scalaVersions = scala2And3Versions,
settings = commonJvmSettings ++ Seq {
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-blaze-server" % Versions.http4sBlazeServer % Test
"org.http4s" %%% "http4s-ember-server" % Versions.http4s % Test
)
}
)
Expand All @@ -1342,7 +1342,7 @@ lazy val http4sServerZio: ProjectMatrix = (projectMatrix in file("server/http4s-
name := "tapir-http4s-server-zio",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer % Test
"org.http4s" %% "http4s-ember-server" % Versions.http4s % Test
)
)
.jvmPlatform(scalaVersions = scala2And3Versions, settings = commonJvmSettings)
Expand Down Expand Up @@ -1910,7 +1910,7 @@ lazy val http4sClient: ProjectMatrix = (projectMatrix in file("client/http4s-cli
name := "tapir-http4s-client",
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-core" % Versions.http4s,
"org.http4s" %% "http4s-blaze-client" % Versions.http4sBlazeClient % Test,
"org.http4s" %% "http4s-ember-client" % Versions.http4s % Test,
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared % Optional
)
)
Expand Down Expand Up @@ -2072,7 +2072,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
"com.github.jwt-scala" %% "jwt-circe" % Versions.jwtScala,
"org.http4s" %% "http4s-dsl" % Versions.http4s,
"org.http4s" %% "http4s-circe" % Versions.http4s,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer,
"org.http4s" %% "http4s-ember-server" % Versions.http4s,
"org.mock-server" % "mockserver-netty" % Versions.mockServer,
"io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry,
Expand Down Expand Up @@ -2143,7 +2143,7 @@ lazy val documentation: ProjectMatrix = (projectMatrix in file("generated-doc"))
name := "doc",
libraryDependencies ++= Seq(
"org.playframework" %% "play-netty-server" % Versions.playServer,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer,
"org.http4s" %% "http4s-ember-server" % Versions.http4s,
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % Versions.sttpApispec,
"com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % Versions.sttpApispec
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package sttp.tapir.client.http4s

import cats.effect.IO
import org.http4s.blaze.client.BlazeClientBuilder
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.{Request, Response, Uri}
import sttp.tapir.client.tests.ClientTests
import sttp.tapir.{DecodeResult, Endpoint}
import scala.concurrent.ExecutionContext.global

abstract class Http4sClientTests[R] extends ClientTests[R] {
override def send[A, I, E, O](
Expand Down Expand Up @@ -35,7 +34,7 @@ abstract class Http4sClientTests[R] extends ClientTests[R] {
}

private def sendAndParseResponse[Result](request: Request[IO], parseResponse: Response[IO] => IO[Result]) =
BlazeClientBuilder[IO](global).resource.use { client =>
EmberClientBuilder.default[IO].build.use { client =>
client.run(request).use(parseResponse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,35 @@ package sttp.tapir.client.tests

import cats.effect._
import cats.effect.std.Queue
import cats.effect.unsafe.implicits.global
import cats.implicits._
import com.comcast.ip4s.Port
import fs2.{Pipe, Stream}
import org.http4s.dsl.io._
import org.http4s.headers.{Accept, `Content-Type`}
import org.http4s.server.Router
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.middleware._
import org.http4s.server.websocket.WebSocketBuilder2
import org.http4s.websocket.WebSocketFrame
import org.http4s._
import org.slf4j.LoggerFactory
import org.typelevel.ci.CIString
import scodec.bits.ByteVector
import sttp.tapir.client.tests.HttpServer._

import scala.concurrent.ExecutionContext
object HttpServer extends ResourceApp.Forever {

object HttpServer {
type Port = Int
private val defaultPort = Port.fromInt(51823).get

def main(args: Array[String]): Unit = {
val port = args.headOption.map(_.toInt).getOrElse(51823)
new HttpServer(port).start()
def run(args: List[String]): Resource[IO, Unit] = {
val port = args.headOption.flatMap(Port.fromString).getOrElse(defaultPort)
new HttpServer(port).build.void
}
}

class HttpServer(port: Port) {

private val logger = LoggerFactory.getLogger(getClass)

private var stopServer: IO[Unit] = _

//

private object numParam extends QueryParamDecoderMatcher[Int]("num")
Expand Down Expand Up @@ -75,7 +71,7 @@ class HttpServer(port: Port) {
case r @ POST -> Root / "api" / "echo" / "multipart" =>
r.decode[multipart.Multipart[IO]] { mp =>
val parts: Vector[multipart.Part[IO]] = mp.parts
def toString(s: fs2.Stream[IO, Byte]): IO[String] = s.through(fs2.text.utf8Decode).compile.foldMonoid
def toString(s: fs2.Stream[IO, Byte]): IO[String] = s.through(fs2.text.utf8.decode).compile.foldMonoid
def partToString(name: String): IO[String] = parts.find(_.name.contains(name)).map(p => toString(p.body)).getOrElse(IO.pure(""))
partToString("fruit").product(partToString("amount")).flatMap { case (fruit, amount) =>
Ok(s"$fruit=$amount")
Expand Down Expand Up @@ -210,25 +206,12 @@ class HttpServer(port: Port) {
Router("/" -> corsService).orNotFound
}

//

def start(): Unit = {
val (_, _stopServer) = BlazeServerBuilder[IO]
.withExecutionContext(ExecutionContext.global)
.bindHttp(port)
.withHttpWebSocketApp(app)
.resource
.map(_.address.getPort)
.allocated
.unsafeRunSync()

stopServer = _stopServer
def build: Resource[IO, server.Server] = EmberServerBuilder
.default[IO]
.withPort(port)
.withHttpWebSocketApp(app)
.build
.evalTap(_ => IO(logger.info(s"Server on port $port started")))
.onFinalize(IO(logger.info(s"Server on port $port stopped")))

logger.info(s"Server on port $port started")
}

def close(): Unit = {
stopServer.unsafeRunSync()
logger.info(s"Server on port $port stopped")
}
}
17 changes: 7 additions & 10 deletions doc/server/http4s.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ The capability can be added to the classpath independently of the interpreter th
## Http4s backends

Http4s integrates with a couple of [server backends](https://http4s.org/v1.0/integrations/), the most popular being
Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Blaze, but other backends can be used
Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Ember, but other backends can be used
as well. This means adding another dependency, such as:

```scala
"org.http4s" %% "http4s-blaze-server" % Http4sVersion
"org.http4s" %% "http4s-ember-server" % Http4sVersion
```

## Web sockets
Expand All @@ -75,24 +75,21 @@ import sttp.tapir.*
import sttp.tapir.server.http4s.Http4sServerInterpreter
import cats.effect.IO
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.http4s.server.websocket.WebSocketBuilder2
import fs2.*
import scala.concurrent.ExecutionContext

given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global

val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, String], Fs2Streams[IO] with WebSockets] =
endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](Fs2Streams[IO]))

val wsRoutes: WebSocketBuilder2[IO] => HttpRoutes[IO] =
Http4sServerInterpreter[IO]().toWebSocketRoutes(wsEndpoint.serverLogicSuccess[IO](_ => ???))

BlazeServerBuilder[IO]
.withExecutionContext(summon[ExecutionContext])
.bindHttp(8080, "localhost")
.withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound)

EmberServerBuilder
.default[IO]
.withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound)
```

```{note}
Expand Down
18 changes: 7 additions & 11 deletions doc/server/zio-http4s.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ The capability can be added to the classpath independently of the interpreter th
## Http4s backends

Http4s integrates with a couple of [server backends](https://http4s.org/v1.0/integrations/), the most popular being
Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Blaze, but other backends can be used
Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Ember, but other backends can be used
as well. This means adding another dependency, such as:

```scala
"org.http4s" %% "http4s-blaze-server" % Http4sVersion
"org.http4s" %% "http4s-ember-server" % Http4sVersion
```

## Web sockets
Expand All @@ -121,7 +121,7 @@ import sttp.tapir.{CodecFormat, PublicEndpoint}
import sttp.tapir.ztapir.*
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.http4s.server.websocket.WebSocketBuilder2
import scala.concurrent.ExecutionContext
Expand All @@ -131,8 +131,6 @@ import zio.stream.Stream

def runtime: Runtime[Any] = ??? // provided by ZIOAppDefault

given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global

val wsEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, String] => Stream[Throwable, String], ZioStreams with WebSockets] =
endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](ZioStreams))

Expand All @@ -141,13 +139,11 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] =

val serve: Task[Unit] =
ZIO.executor.flatMap(executor =>
BlazeServerBuilder[Task]
.withExecutionContext(executor.asExecutionContext)
.bindHttp(8080, "localhost")
EmberServerBuilder
.default[Task]
.withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound)
.serve
.compile
.drain
.build
.useForever
)
```

Expand Down
32 changes: 15 additions & 17 deletions doc/tutorials/07_cats_effect.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ standard code to start a server and handle requests until the application is int
```scala
//> using dep com.softwaremill.sttp.tapir::tapir-core:@VERSION@
//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:@VERSION@
//> using dep org.http4s::http4s-blaze-server:0.23.16
//> using dep org.http4s::http4s-ember-server:0.23.30

import cats.effect.{ExitCode, IO, IOApp}
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import sttp.tapir.*
import sttp.tapir.server.http4s.Http4sServerInterpreter
Expand All @@ -154,12 +154,11 @@ object HelloWorldTapir extends IOApp:
.toRoutes(helloWorldEndpoint)

override def run(args: List[String]): IO[ExitCode] =
BlazeServerBuilder[IO]
.bindHttp(8080, "localhost")
EmberServerBuilder
.default[IO]
.withHttpApp(Router("/" -> helloWorldRoutes).orNotFound)
.resource
.use(_ => IO.never)
.as(ExitCode.Success)
.build
.useForever
```

First of all, you might notice that instead of the `@main` method, we are extending the `IOApp` trait. This is needed,
Expand All @@ -169,8 +168,8 @@ the `IOApp` will handle evaluating the `IO` description and actually running the

Secondly, with http4s we need to use a specific server implementation (http4s itself is only an API to define endpoints -
kind of a middle-man between Tapir and low-level networking code). We can choose from `blaze` and `ember` servers, here
we're using the `blaze` one, which is reflected in the additional dependency and the server configuration constructor:
`BlazeServerBuilder`.
we're using the `ember` one, which is reflected in the additional dependency and the server configuration constructor:
`EmberServerBuilder`.

Finally, we've got the `run` method implementation, which attaches our interpreted route to the root context `/` and
exposes the server on `localhost:8080`.
Expand All @@ -195,12 +194,12 @@ the second step that we need to perform:
//> using dep com.softwaremill.sttp.tapir::tapir-core:@VERSION@
//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:@VERSION@
//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:@VERSION@
//> using dep org.http4s::http4s-blaze-server:0.23.16
//> using dep org.http4s::http4s-ember-server:0.23.30

import cats.effect.{ExitCode, IO, IOApp}
import cats.syntax.all.*
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import sttp.tapir.*
import sttp.tapir.server.http4s.Http4sServerInterpreter
Expand All @@ -226,17 +225,16 @@ object HelloWorldTapir extends IOApp:
val allRoutes: HttpRoutes[IO] = helloWorldRoutes <+> swaggerRoutes

override def run(args: List[String]): IO[ExitCode] =
BlazeServerBuilder[IO]
.bindHttp(8080, "localhost")
EmberServerBuilder
.default[IO]
.withHttpApp(Router("/" -> allRoutes).orNotFound)
.resource
.use(_ => IO.never)
.as(ExitCode.Success)
.build
.useForever
```

Hence, we first generate endpoint descriptions, which correspond to exposing the Swagger UI (containing the generated
OpenAPI yaml for our `/hello/world` endpoint), which use `IO` to express their server logic. Then, we interpret those
endpoints as `HttpRoutes[IO]`, which we can expose using http4's blaze server.
endpoints as `HttpRoutes[IO]`, which we can expose using http4's ember server.

## Other concepts covered so far

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package sttp.tapir.redoc.bundle

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.comcast.ip4s.Port
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.scalatest.Assertion
import org.scalatest.funsuite.AsyncFunSuite
Expand Down Expand Up @@ -66,10 +67,11 @@ class RedocInterpreterTest extends AsyncFunSuite with Matchers {
.fromEndpoints[IO](List(testEndpoint), "The tapir library", "1.0.0")
)

BlazeServerBuilder[IO]
.bindHttp(0, "localhost")
EmberServerBuilder
.default[IO]
.withPort(Port.fromInt(0).get)
.withHttpApp(Router(s"/${context.mkString("/")}" -> redocUIRoutes).orNotFound)
.resource
.build
.use { server =>
IO {
val port = server.address.getPort
Expand Down
Loading
Loading