From 8b5ea8a7140f8cca08588250bfa6e2ccb9d8c709 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Sat, 1 Feb 2025 14:24:17 +0100 Subject: [PATCH 1/8] http4s: Migrate from blaze to ember --- build.sbt | 18 +++++------ .../client/http4s/Http4sClientTests.scala | 5 ++-- .../sttp/tapir/client/tests/HttpServer.scala | 28 ++++++++--------- doc/server/http4s.md | 17 +++++------ doc/server/zio-http4s.md | 20 +++++-------- doc/tutorials/07_cats_effect.md | 29 +++++++++--------- .../redoc/bundle/RedocInterpreterTest.scala | 10 ++++--- .../bundle/SwaggerInterpreterTest.scala | 10 ++++--- .../examples/HelloWorldHttp4sServer.scala | 11 ++++--- .../examples/ZioEnvExampleHttp4sServer.scala | 22 +++++--------- .../examples/ZioExampleHttp4sServer.scala | 19 +++++------- .../ZioPartialServerLogicHttp4s.scala | 22 +++++++------- .../examples/client/Http4sClientExample.scala | 2 +- .../errors/ErrorUnionTypesHttp4sServer.scala | 13 ++++---- ...leEndpointsDocumentationHttp4sServer.scala | 11 ++++--- .../RedocContextPathHttp4sServer.scala | 11 ++++--- .../security/OAuth2GithubHttp4sServer.scala | 11 ++++--- .../streaming/ProxyHttp4sFs2Server.scala | 10 +++---- .../streaming/StreamingHttp4sFs2Server.scala | 10 +++---- .../StreamingHttp4sFs2ServerOrError.scala | 11 ++++--- .../websocket/WebSocketHttp4sServer.scala | 11 ++++--- generated-doc/out/server/http4s.md | 11 ++++--- generated-doc/out/server/zio-http4s.md | 18 +++++------ generated-doc/out/tutorials/07_cats_effect.md | 30 +++++++++---------- .../scala/sttp/tapir/perf/http4s/Http4s.scala | 12 ++++---- project/Versions.scala | 2 -- .../http4s/Http4sServerInterpreter.scala | 2 +- .../server/http4s/Http4sServerTest.scala | 12 ++++---- .../http4s/Http4sTestServerInterpreter.scala | 21 ++++++------- .../ztapir/ZHttp4sTestServerInterpreter.scala | 17 ++++++----- 30 files changed, 196 insertions(+), 230 deletions(-) diff --git a/build.sbt b/build.sbt index ed55381b9f..c937a9a956 100644 --- a/build.sbt +++ b/build.sbt @@ -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 ), @@ -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 ), @@ -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 ) ) @@ -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 ) ) @@ -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 ) } ) @@ -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) @@ -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 ) ) @@ -2073,7 +2073,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, @@ -2144,7 +2144,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 ), diff --git a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala index 4b81740e65..d460da66e2 100644 --- a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala +++ b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala @@ -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]( @@ -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) } } diff --git a/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala b/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala index ccf68fdd46..8a9d2a362b 100644 --- a/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala +++ b/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala @@ -2,13 +2,13 @@ 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 @@ -17,18 +17,17 @@ import org.slf4j.LoggerFactory import org.typelevel.ci.CIString import scodec.bits.ByteVector -import scala.concurrent.ExecutionContext - object HttpServer extends ResourceApp.Forever { - type Port = Int + + private val defaultPort = Port.fromInt(51823).get def run(args: List[String]): Resource[IO, Unit] = { - val port = args.headOption.map(_.toInt).getOrElse(51823) + val port = args.headOption.flatMap(Port.fromString).getOrElse(defaultPort) new HttpServer(port).build.void } } -class HttpServer(port: HttpServer.Port) { +class HttpServer(port: Port) { private val logger = LoggerFactory.getLogger(getClass) @@ -207,13 +206,12 @@ class HttpServer(port: HttpServer.Port) { Router("/" -> corsService).orNotFound } - // + 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"))) - def build: Resource[IO, server.Server] = BlazeServerBuilder[IO] - .withExecutionContext(ExecutionContext.global) - .bindHttp(port) - .withHttpWebSocketApp(app) - .resource - .evalTap(_ => IO(logger.info(s"Server on port $port started"))) - .onFinalize(IO(logger.info(s"Server on port $port stopped"))) } diff --git a/doc/server/http4s.md b/doc/server/http4s.md index c5db5d59b1..2b53209c38 100644 --- a/doc/server/http4s.md +++ b/doc/server/http4s.md @@ -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 @@ -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} diff --git a/doc/server/zio-http4s.md b/doc/server/zio-http4s.md index 0bf2f7852a..5960e0d97f 100644 --- a/doc/server/zio-http4s.md +++ b/doc/server/zio-http4s.md @@ -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 @@ -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 @@ -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)) @@ -141,14 +139,12 @@ 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 + ) ``` ## Server Sent Events diff --git a/doc/tutorials/07_cats_effect.md b/doc/tutorials/07_cats_effect.md index 2024e2bc7d..ba84b1be71 100644 --- a/doc/tutorials/07_cats_effect.md +++ b/doc/tutorials/07_cats_effect.md @@ -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 @@ -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, @@ -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`. @@ -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 @@ -226,16 +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 + .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 diff --git a/docs/redoc-bundle/src/test/scala/sttp/tapir/redoc/bundle/RedocInterpreterTest.scala b/docs/redoc-bundle/src/test/scala/sttp/tapir/redoc/bundle/RedocInterpreterTest.scala index 81e36d3412..97b75ff91a 100644 --- a/docs/redoc-bundle/src/test/scala/sttp/tapir/redoc/bundle/RedocInterpreterTest.scala +++ b/docs/redoc-bundle/src/test/scala/sttp/tapir/redoc/bundle/RedocInterpreterTest.scala @@ -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 @@ -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 diff --git a/docs/swagger-ui-bundle/src/test/scala/sttp/tapir/swagger/bundle/SwaggerInterpreterTest.scala b/docs/swagger-ui-bundle/src/test/scala/sttp/tapir/swagger/bundle/SwaggerInterpreterTest.scala index 0eaad61e02..a1b984ca8b 100644 --- a/docs/swagger-ui-bundle/src/test/scala/sttp/tapir/swagger/bundle/SwaggerInterpreterTest.scala +++ b/docs/swagger-ui-bundle/src/test/scala/sttp/tapir/swagger/bundle/SwaggerInterpreterTest.scala @@ -2,8 +2,9 @@ package sttp.tapir.swagger.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 @@ -33,10 +34,11 @@ class SwaggerInterpreterTest 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("/")}" -> swaggerUIRoutes).orNotFound) - .resource + .build .use { server => IO { val port = server.address.getPort diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 20a39e23d3..fe4a175f25 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -3,14 +3,14 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.client3::core:3.9.8 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples import cats.effect.* 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.client3.* import sttp.shared.Identity @@ -33,11 +33,10 @@ object HelloWorldHttp4sServer extends IOApp: override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) - .resource + .build .use { _ => IO { val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index befa7c713c..0dcfd4016f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -6,7 +6,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples @@ -14,7 +14,7 @@ package sttp.tapir.examples import cats.syntax.all.* import io.circe.generic.auto.* import org.http4s.* -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.tapir.PublicEndpoint import sttp.tapir.generic.auto.* @@ -71,17 +71,11 @@ object ZioEnvExampleHttp4sServer extends ZIOAppDefault: .toRoutes // Starting the server - val serve: ZIO[PetService, Throwable, Unit] = { - ZIO.executor.flatMap(executor => - BlazeServerBuilder[RIO[PetService, *]] - .withExecutionContext(executor.asExecutionContext) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) - .serve - .compile - .drain - ) - - } + val serve: ZIO[PetService, Throwable, Unit] = + EmberServerBuilder + .default[RIO[PetService, *]] + .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) + .build + .useForever override def run: URIO[Any, ExitCode] = serve.provide(PetService.live).exitCode diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index cf41273194..45831022f3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -5,7 +5,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples @@ -13,7 +13,7 @@ package sttp.tapir.examples import cats.syntax.all.* import io.circe.generic.auto.* import org.http4s.* -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.tapir.PublicEndpoint import sttp.tapir.generic.auto.* @@ -59,15 +59,10 @@ object ZioExampleHttp4sServer extends ZIOAppDefault: .toRoutes // Starting the server - val serve: Task[Unit] = - ZIO.executor.flatMap(executor => - BlazeServerBuilder[Task] - .withExecutionContext(executor.asExecutionContext) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) - .serve - .compile - .drain - ) + val serve: Task[Unit] = EmberServerBuilder + .default[Task] + .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) + .build + .useForever override def run: URIO[Any, ExitCode] = serve.exitCode diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 783ea375ee..7aab61bfd7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -4,13 +4,13 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.2 package sttp.tapir.examples import org.http4s.* -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.client3.* import sttp.client3.asynchttpclient.zio.AsyncHttpClientZioBackend @@ -78,16 +78,14 @@ object ZioPartialServerLogicHttp4s extends ZIOAppDefault: // override def run: URIO[Any, ExitCode] = - ZIO.executor.flatMap(executor => - BlazeServerBuilder[RIO[UserService, *]] - .withExecutionContext(executor.asExecutionContext) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) - .resource - .use(_ => test) - .provide(UserService.live) - .exitCode - ) + EmberServerBuilder + .default[RIO[UserService, *]] + .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) + .build + .use(_ => test) + .provide(UserService.live) + .exitCode + end ZioPartialServerLogicHttp4s object UserAuthenticationLayer: diff --git a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala index cd2b12293a..2473b5e098 100644 --- a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -4,7 +4,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-http4s-client:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.13 //> using dep org.http4s::http4s-circe:0.23.27 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep org.http4s::http4s-dsl:0.23.27 package sttp.tapir.examples.client diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala index a0019cfce8..a68d5cd41d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.errors @@ -11,7 +11,7 @@ package sttp.tapir.examples.errors import cats.effect.* import io.circe.generic.auto.* import org.http4s.HttpRoutes -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.client3.* import sttp.model.StatusCode @@ -67,15 +67,12 @@ object ErrorUnionTypesHttp4sServer extends IOApp: // converting an endpoint to a route (providing server-side logic); extension method comes from imported packages val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(helloServerEndpoint) - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) - .resource + .build .use { _ => IO { val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala index 60cd464a97..961d993e9e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -4,7 +4,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.openapi @@ -12,7 +12,7 @@ import cats.effect.* import cats.syntax.all.* import io.circe.generic.auto.* 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.generic.auto.* @@ -73,11 +73,10 @@ object MultipleEndpointsDocumentationHttp4sServer extends IOApp: override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> (routes)).orNotFound) - .resource + .build .use { _ => IO { println("Go to: http://localhost:8080/docs") diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala index fde21f3898..1c3d19ec8c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -3,15 +3,15 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.openapi import cats.effect.* import cats.syntax.all.* import org.http4s.HttpRoutes +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router -import org.http4s.blaze.server.BlazeServerBuilder import sttp.tapir.* import sttp.tapir.redoc.RedocUIOptions import sttp.tapir.redoc.bundle.RedocInterpreter @@ -37,10 +37,9 @@ object RedocContextPathHttp4sServer extends IOApp: override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router(s"/${contextPath.mkString("/")}" -> routes).orNotFound) - .resource + .build .use { _ => IO.println(s"go to: http://127.0.0.1:8080/${(contextPath ++ docPathPrefix).mkString("/")}") *> IO.never } .as(ExitCode.Success) diff --git a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala index 55300bb83a..5f2914daa7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -4,7 +4,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.13 //> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.10.2 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 //> using dep com.github.jwt-scala::jwt-circe:10.0.1 package sttp.tapir.examples.security @@ -13,8 +13,8 @@ import cats.effect.* import cats.syntax.all.* import io.circe.generic.auto.* import org.http4s.HttpRoutes +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router -import org.http4s.blaze.server.BlazeServerBuilder import pdi.jwt.{JwtAlgorithm, JwtCirce, JwtClaim} import sttp.client3.* import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend @@ -118,11 +118,10 @@ object OAuth2GithubHttp4sServer extends IOApp: // starting the server httpClient .use(backend => - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> (secretPlaceRoute <+> loginRoute <+> loginGithubRoute(backend))).orNotFound) - .resource + .build .use { _ => IO { println("Go to: http://localhost:8080") diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala index 8ea90f2f64..002d3f4a91 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -3,14 +3,14 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.client3::fs2:3.9.8 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.streaming import cats.effect.{ExitCode, IO, IOApp} import fs2.Stream import org.http4s.HttpRoutes -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.capabilities.fs2.Fs2Streams import sttp.client3.* @@ -58,8 +58,8 @@ object ProxyHttp4sFs2Server extends IOApp: (for { backend <- HttpClientFs2Backend.resource[IO]() routes = proxyRoutes(backend) - _ <- BlazeServerBuilder[IO] - .bindHttp(8080, "localhost") + _ <- EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> routes).orNotFound) - .resource + .build } yield ()).useForever diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala index 2b11164109..6231268d62 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.client3::core:3.9.8 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.streaming @@ -11,7 +11,7 @@ import cats.effect.{ExitCode, IO, IOApp} import cats.implicits.* import fs2.{Chunk, Stream} import org.http4s.HttpRoutes -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.capabilities.fs2.Fs2Streams import sttp.client3.* @@ -52,10 +52,10 @@ object StreamingHttp4sFs2Server extends IOApp: override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> streamingRoutes).orNotFound) - .resource + .build .use { _ => IO { val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala index 4d8e6079de..3bd593f156 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala @@ -2,13 +2,13 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.streaming import cats.effect.* import org.http4s.HttpRoutes -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import sttp.capabilities.fs2.Fs2Streams import sttp.model.StatusCode @@ -51,9 +51,8 @@ object StreamingHttp4sFs2ServerOrError extends IOApp: // curl -v http://localhost:8080/user/another_user (responds with 404) override def run(args: List[String]): IO[ExitCode] = // starting the server - BlazeServerBuilder[IO] - .withExecutionContext(scala.concurrent.ExecutionContext.global) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpApp(Router("/" -> userDataRoutes).orNotFound) - .resource + .build .useForever diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala index 54dd4c2f95..3712c57f80 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala @@ -6,7 +6,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.13 //> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 //> using dep com.softwaremill.sttp.client3::async-http-client-backend-fs2:3.10.2 -//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep org.http4s::http4s-ember-server:0.23.30 package sttp.tapir.examples.websocket @@ -14,7 +14,7 @@ import cats.effect.{ExitCode, IO, IOApp} import io.circe.generic.auto.* import fs2.* 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 sttp.apispec.asyncapi.Server @@ -81,11 +81,10 @@ object WebSocketHttp4sServer extends IOApp: override def run(args: List[String]): IO[ExitCode] = // Starting the server - BlazeServerBuilder[IO] - .withExecutionContext(ec) - .bindHttp(8080, "localhost") + EmberServerBuilder + .default[IO] .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) - .resource + .build .flatMap(_ => AsyncHttpClientFs2Backend.resource[IO]()) .use { backend => // Client which interacts with the web socket diff --git a/generated-doc/out/server/http4s.md b/generated-doc/out/server/http4s.md index 5f12a69d0a..c686f154b0 100644 --- a/generated-doc/out/server/http4s.md +++ b/generated-doc/out/server/http4s.md @@ -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 @@ -75,7 +75,7 @@ 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.* @@ -89,9 +89,8 @@ val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, String], Fs2Streams[ val wsRoutes: WebSocketBuilder2[IO] => HttpRoutes[IO] = Http4sServerInterpreter[IO]().toWebSocketRoutes(wsEndpoint.serverLogicSuccess[IO](_ => ???)) -BlazeServerBuilder[IO] - .withExecutionContext(summon[ExecutionContext]) - .bindHttp(8080, "localhost") +EmberServerBuilder + .default[IO] .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) ``` diff --git a/generated-doc/out/server/zio-http4s.md b/generated-doc/out/server/zio-http4s.md index 03360fb1f9..be7348c65d 100644 --- a/generated-doc/out/server/zio-http4s.md +++ b/generated-doc/out/server/zio-http4s.md @@ -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 @@ -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 @@ -140,15 +140,11 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = ZHttp4sServerInterpreter().fromWebSocket(wsEndpoint.zServerLogic(_ => ???)).toRoutes 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 ``` ## Server Sent Events diff --git a/generated-doc/out/tutorials/07_cats_effect.md b/generated-doc/out/tutorials/07_cats_effect.md index 4ac6669866..40e10b5f85 100644 --- a/generated-doc/out/tutorials/07_cats_effect.md +++ b/generated-doc/out/tutorials/07_cats_effect.md @@ -132,11 +132,10 @@ standard code to start a server and handle requests until the application is int ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 -//> 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 @@ -154,12 +153,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, @@ -169,8 +167,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`. @@ -195,12 +193,12 @@ the second step that we need to perform: //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.13 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.13 -//> 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 @@ -226,16 +224,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 + .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 diff --git a/perf-tests/src/main/scala/sttp/tapir/perf/http4s/Http4s.scala b/perf-tests/src/main/scala/sttp/tapir/perf/http4s/Http4s.scala index 7fb0fa795c..159c479e4d 100644 --- a/perf-tests/src/main/scala/sttp/tapir/perf/http4s/Http4s.scala +++ b/perf-tests/src/main/scala/sttp/tapir/perf/http4s/Http4s.scala @@ -2,10 +2,11 @@ package sttp.tapir.perf.http4s import cats.effect._ import cats.syntax.all._ +import com.comcast.ip4s import fs2._ import fs2.io.file.{Files, Path => Fs2Path} import org.http4s._ -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.dsl._ import org.http4s.implicits._ import org.http4s.server.Router @@ -105,14 +106,13 @@ object Tapir extends Endpoints { object server { val maxConnections = 65536 - val connectorPoolSize: Int = Math.max(2, Runtime.getRuntime.availableProcessors() / 4) def runServer(router: WebSocketBuilder2[IO] => HttpRoutes[IO]): Resource[IO, Unit] = - BlazeServerBuilder[IO] - .bindHttp(Port, "localhost") + EmberServerBuilder + .default[IO] + .withPort(ip4s.Port.fromInt(Port).get) .withHttpWebSocketApp(wsb => router(wsb).orNotFound) .withMaxConnections(maxConnections) - .withConnectorPoolSize(connectorPoolSize) - .resource + .build .map(_ => ()) .onFinalize(IO.println("Http4s server closed.")) } diff --git a/project/Versions.scala b/project/Versions.scala index c12ff0e482..dd20415401 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,7 +1,5 @@ object Versions { val http4s = "0.23.30" - val http4sBlazeServer = "0.23.17" - val http4sBlazeClient = "0.23.17" val catsCore = "2.13.0" val catsEffect = "3.5.7" val circe = "0.14.10" diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala index 64df3e9f66..902c0788e8 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala @@ -124,7 +124,7 @@ trait Http4sServerInterpreter[F[_]] { new Http4sInvalidWebSocketUse( "Invalid usage of web socket endpoint without WebSocketBuilder2. " + "Use the toWebSocketRoutes/toWebSocketHttp interpreter methods, " + - "and add the result using BlazeServerBuilder.withHttpWebSocketApp(..)." + "and add the result using (Blaze/Ember)ServerBuilder.withHttpWebSocketApp(..)." ) ) } diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala index 4a69014f2c..f642107856 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -4,9 +4,10 @@ import cats.data._ import cats.effect._ import cats.effect.unsafe.implicits.global import cats.syntax.all._ +import com.comcast.ip4s.Port import fs2.Pipe import fs2.Stream -import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import org.http4s.server.ContextMiddleware import org.http4s.ContextRoutes @@ -24,11 +25,12 @@ import sttp.tapir.tests.{Test, TestSuite} import sttp.ws.{WebSocket, WebSocketFrame} import java.util.UUID -import scala.concurrent.ExecutionContext import scala.concurrent.duration.DurationInt import scala.util.Random class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite with OptionValues { + private val anyAvailablePort = Port.fromInt(0).get + private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) override def tests: Resource[IO, List[Test]] = backendResource.map { backend => implicit val m: CatsMonadError[IO] = new CatsMonadError[IO] @@ -40,11 +42,9 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val sse2 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) def assert_get_apiTestRouter_respondsWithExpectedContent[T](routes: HttpRoutes[IO], expectedContext: T): IO[Assertion] = - BlazeServerBuilder[IO] - .withExecutionContext(ExecutionContext.global) - .bindHttp(0, "localhost") + serverBuilder .withHttpApp(Router("/api" -> routes).orNotFound) - .resource + .build .use { server => val port = server.address.getPort basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right(expectedContext)) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala index a5852ce240..23a63ac525 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -3,7 +3,8 @@ package sttp.tapir.server.http4s import cats.data.NonEmptyList import cats.effect.{IO, Resource} import cats.syntax.all._ -import org.http4s.blaze.server.BlazeServerBuilder +import com.comcast.ip4s +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.websocket.WebSocketBuilder2 import org.http4s.{HttpApp, HttpRoutes} import sttp.capabilities.WebSockets @@ -13,7 +14,6 @@ import sttp.tapir.server.http4s.Http4sTestServerInterpreter._ import sttp.tapir.server.tests.TestServerInterpreter import sttp.tapir.tests._ -import scala.concurrent.ExecutionContext import scala.concurrent.duration._ object Http4sTestServerInterpreter { @@ -21,25 +21,26 @@ object Http4sTestServerInterpreter { } class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[IO] with WebSockets, Http4sServerOptions[IO], Routes] { - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global override def route(es: List[ServerEndpoint[Fs2Streams[IO] with WebSockets, IO]], interceptors: Interceptors): Routes = { val serverOptions: Http4sServerOptions[IO] = interceptors(Http4sServerOptions.customiseInterceptors[IO]).options Http4sServerInterpreter(serverOptions).toWebSocketRoutes(es) } + private val anyAvailablePort = ip4s.Port.fromInt(0).get + private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) + override def server( routes: NonEmptyList[Routes], gracefulShutdownTimeout: Option[FiniteDuration] ): Resource[IO, Port] = { val service: WebSocketBuilder2[IO] => HttpApp[IO] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - - BlazeServerBuilder[IO] - .withExecutionContext(ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpWebSocketApp(service) - .resource - .map(_.address.getPort()) + gracefulShutdownTimeout + .foldLeft(serverBuilder.withHttpWebSocketApp(service)) { case (b, t) => + b.withShutdownTimeout(t) + } + .build + .map(_.address.getPort) } } diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala index 389613fbb5..ecb47fba5e 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala @@ -4,7 +4,8 @@ import cats.data.NonEmptyList import cats.effect.{IO, Resource} import cats._ import cats.syntax.all._ -import org.http4s.blaze.server.BlazeServerBuilder +import com.comcast.ip4s +import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.websocket.WebSocketBuilder2 import org.http4s.{HttpApp, HttpRoutes} import sttp.capabilities.WebSockets @@ -30,6 +31,9 @@ object ZHttp4sTestServerInterpreter { class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStreams with WebSockets, ServerOptions, Routes] { + private val anyAvailablePort = ip4s.Port.fromInt(0).get + private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort) + override def route(es: List[ZServerEndpoint[Any, ZioStreams with WebSockets]], interceptors: Interceptors): Routes = { val serverOptions: ServerOptions = interceptors(Http4sServerOptions.customiseInterceptors[Task]).options ZHttp4sServerInterpreter(serverOptions).fromWebSocket(es).toRoutes @@ -41,12 +45,11 @@ class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStream ): Resource[IO, Port] = { val service: WebSocketBuilder2[Task] => HttpApp[Task] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - - BlazeServerBuilder[Task] - .withExecutionContext(ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpWebSocketApp(service) - .resource + gracefulShutdownTimeout + .foldLeft(serverBuilder.withHttpWebSocketApp(service)) { case (b, t) => + b.withShutdownTimeout(t) + } + .build .map(_.address.getPort) .mapK(new ~>[Task, IO] { // Converting a ZIO effect to an Cats Effect IO effect From a6837628703009bff4bea3ee9c2c3d115956d58a Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Sat, 1 Feb 2025 22:55:29 +0100 Subject: [PATCH 2/8] connection idle timeout --- doc/server/zio-http4s.md | 12 +++++------- .../MultipleEndpointsDocumentationHttp4sServer.scala | 12 +++--------- .../sttp/tapir/server/http4s/Http4sServerTest.scala | 6 +++--- .../server/http4s/Http4sTestServerInterpreter.scala | 3 ++- .../http4s/ztapir/ZHttp4sTestServerInterpreter.scala | 6 ++++-- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/doc/server/zio-http4s.md b/doc/server/zio-http4s.md index 5960e0d97f..30bccbeb95 100644 --- a/doc/server/zio-http4s.md +++ b/doc/server/zio-http4s.md @@ -138,13 +138,11 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = ZHttp4sServerInterpreter().fromWebSocket(wsEndpoint.zServerLogic(_ => ???)).toRoutes val serve: Task[Unit] = - ZIO.executor.flatMap(executor => - EmberServerBuilder - .default[Task] - .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) - .build - .useForever - ) + EmberServerBuilder + .default[Task] + .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) + .build + .useForever ``` ## Server Sent Events diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala index 961d993e9e..cef6dd6be0 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -43,7 +43,6 @@ object MultipleEndpointsDocumentationHttp4sServer extends IOApp: ) // server-side logic - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val books = new AtomicReference( Vector( @@ -75,13 +74,8 @@ object MultipleEndpointsDocumentationHttp4sServer extends IOApp: // starting the server EmberServerBuilder .default[IO] - .withHttpApp(Router("/" -> (routes)).orNotFound) + .withHttpApp(Router("/" -> routes).orNotFound) .build - .use { _ => - IO { - println("Go to: http://localhost:8080/docs") - println("Press any key to exit ...") - scala.io.StdIn.readLine() - } - } + .evalTap(_ => IO.println("Go to: http://localhost:8080/docs")) + .surround(IO.println("Press any key to exit ...") *> IO.readLine) .as(ExitCode.Success) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala index f642107856..c4d041e72a 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -56,7 +56,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val e = endpoint.get.in("test" / "router").out(stringBody).serverLogic(_ => IO.pure(expectedContent.asRight[Unit])) val routes = Http4sServerInterpreter[IO]().toRoutes(e) - assert_get_apiTestRouter_respondsWithExpectedContent(routes, expectedContent).unsafeRunSync() + assert_get_apiTestRouter_respondsWithExpectedContent(routes, expectedContent).unsafeToFuture() }, Test("should work with a router and context routes in a context") { val expectedContext: String = "Hello World!" // the context we expect http4s to provide to the endpoint @@ -73,7 +73,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val middleware: ContextMiddleware[IO, String] = ContextMiddleware.const(expectedContext) - assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext).unsafeRunSync() + assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext).unsafeToFuture() }, Test("should work with a router and context routes in a context using contextSecurityIn") { val expectedContext: Int = 3 @@ -87,7 +87,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val middleware: ContextMiddleware[IO, Int] = ContextMiddleware.const(expectedContext) - assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext.toString).unsafeRunSync() + assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext.toString).unsafeToFuture() }, createServerTest.testServer( endpoint.out( diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala index 23a63ac525..4a74837531 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -28,7 +28,8 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I } private val anyAvailablePort = ip4s.Port.fromInt(0).get - private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) + // FIXME: if connection idle timeout is default, tests are very slow... Closing connection bug? + private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort).withIdleTimeout(50.millis) override def server( routes: NonEmptyList[Routes], diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala index ecb47fba5e..f2b9cff125 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala @@ -21,7 +21,7 @@ import zio.interop.catz._ import zio.interop.catz.implicits._ import scala.concurrent.ExecutionContext -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration.{DurationInt, FiniteDuration} object ZHttp4sTestServerInterpreter { type F[A] = Task[A] @@ -32,7 +32,9 @@ object ZHttp4sTestServerInterpreter { class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStreams with WebSockets, ServerOptions, Routes] { private val anyAvailablePort = ip4s.Port.fromInt(0).get - private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort) + + // FIXME: if connection idle timeout is default, tests are very slow... Closing connection bug? + private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort).withIdleTimeout(50.millis) override def route(es: List[ZServerEndpoint[Any, ZioStreams with WebSockets]], interceptors: Interceptors): Routes = { val serverOptions: ServerOptions = interceptors(Http4sServerOptions.customiseInterceptors[Task]).options From d60df9f607c7a58b1e0d92b9412249c154ebb312 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 3 Feb 2025 11:43:46 +0100 Subject: [PATCH 3/8] Explicit immediate shutdown of test server --- .../server/http4s/Http4sTestServerInterpreter.scala | 13 +++++++------ .../ztapir/ZHttp4sTestServerInterpreter.scala | 13 ++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala index 4a74837531..422c141866 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -28,8 +28,7 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I } private val anyAvailablePort = ip4s.Port.fromInt(0).get - // FIXME: if connection idle timeout is default, tests are very slow... Closing connection bug? - private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort).withIdleTimeout(50.millis) + private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) override def server( routes: NonEmptyList[Routes], @@ -37,10 +36,12 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I ): Resource[IO, Port] = { val service: WebSocketBuilder2[IO] => HttpApp[IO] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - gracefulShutdownTimeout - .foldLeft(serverBuilder.withHttpWebSocketApp(service)) { case (b, t) => - b.withShutdownTimeout(t) - } + + serverBuilder + .withHttpWebSocketApp(service) + .withShutdownTimeout( + gracefulShutdownTimeout.getOrElse(0.seconds) // no need to wait unless it's explicitly required by test + ) .build .map(_.address.getPort) } diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala index f2b9cff125..bafbe571c2 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala @@ -32,9 +32,7 @@ object ZHttp4sTestServerInterpreter { class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStreams with WebSockets, ServerOptions, Routes] { private val anyAvailablePort = ip4s.Port.fromInt(0).get - - // FIXME: if connection idle timeout is default, tests are very slow... Closing connection bug? - private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort).withIdleTimeout(50.millis) + private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort) override def route(es: List[ZServerEndpoint[Any, ZioStreams with WebSockets]], interceptors: Interceptors): Routes = { val serverOptions: ServerOptions = interceptors(Http4sServerOptions.customiseInterceptors[Task]).options @@ -47,10 +45,11 @@ class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStream ): Resource[IO, Port] = { val service: WebSocketBuilder2[Task] => HttpApp[Task] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - gracefulShutdownTimeout - .foldLeft(serverBuilder.withHttpWebSocketApp(service)) { case (b, t) => - b.withShutdownTimeout(t) - } + serverBuilder + .withHttpWebSocketApp(service) + .withShutdownTimeout( + gracefulShutdownTimeout.getOrElse(0.seconds) // no need to wait unless it's explicitly required by test + ) .build .map(_.address.getPort) .mapK(new ~>[Task, IO] { From 99af5d3c3e1721df2f01cecc36728064d4c0282d Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 3 Feb 2025 19:46:14 +0100 Subject: [PATCH 4/8] Less strict check when close frame is not decoded --- .../sttp/tapir/server/tests/ServerWebSocketTests.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala index 3c1d7a3fb3..945db195c4 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala @@ -68,7 +68,11 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE]( .map(_.last) .value .asInstanceOf[Option[Either[WebSocketFrame, String]]] - .forall(_ == Left(WebSocketFrame.Close(1000, "normal closure"))) + .forall { + case Left(WebSocketFrame.Close(1000, "normal closure")) if decodeCloseRequests => true + case Left(WebSocketFrame.Close(1000, "" | "normal closure")) if !decodeCloseRequests => true + case _ => false + } ) } }, From 2b109bc3e1822c3abcfe8d2adfad0ad6648851cb Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Tue, 4 Feb 2025 09:58:19 +0100 Subject: [PATCH 5/8] reuse test interpreter for DRY and quick server resource dealloc --- .../server/http4s/Http4sServerTest.scala | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala index c4d041e72a..d4a7d2bf71 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -4,10 +4,7 @@ import cats.data._ import cats.effect._ import cats.effect.unsafe.implicits.global import cats.syntax.all._ -import com.comcast.ip4s.Port import fs2.Pipe -import fs2.Stream -import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import org.http4s.server.ContextMiddleware import org.http4s.ContextRoutes @@ -29,8 +26,6 @@ import scala.concurrent.duration.DurationInt import scala.util.Random class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite with OptionValues { - private val anyAvailablePort = Port.fromInt(0).get - private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) override def tests: Resource[IO, List[Test]] = backendResource.map { backend => implicit val m: CatsMonadError[IO] = new CatsMonadError[IO] @@ -42,13 +37,12 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val sse2 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) def assert_get_apiTestRouter_respondsWithExpectedContent[T](routes: HttpRoutes[IO], expectedContext: T): IO[Assertion] = - serverBuilder - .withHttpApp(Router("/api" -> routes).orNotFound) - .build - .use { server => - val port = server.address.getPort - basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right(expectedContext)) + interpreter + .server(NonEmptyList.of(_ => Router("/api" -> routes))) + .use { port => + basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend) } + .map(_.body shouldBe Right(expectedContext)) def additionalTests(): List[Test] = List( Test("should work with a router and routes in a context") { @@ -56,7 +50,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val e = endpoint.get.in("test" / "router").out(stringBody).serverLogic(_ => IO.pure(expectedContent.asRight[Unit])) val routes = Http4sServerInterpreter[IO]().toRoutes(e) - assert_get_apiTestRouter_respondsWithExpectedContent(routes, expectedContent).unsafeToFuture() + assert_get_apiTestRouter_respondsWithExpectedContent(routes, expectedContent).unsafeRunSync() }, Test("should work with a router and context routes in a context") { val expectedContext: String = "Hello World!" // the context we expect http4s to provide to the endpoint @@ -73,7 +67,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val middleware: ContextMiddleware[IO, String] = ContextMiddleware.const(expectedContext) - assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext).unsafeToFuture() + assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext).unsafeRunSync() }, Test("should work with a router and context routes in a context using contextSecurityIn") { val expectedContext: Int = 3 @@ -87,7 +81,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val middleware: ContextMiddleware[IO, Int] = ContextMiddleware.const(expectedContext) - assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext.toString).unsafeToFuture() + assert_get_apiTestRouter_respondsWithExpectedContent(middleware(routesWithContext), expectedContext.toString).unsafeRunSync() }, createServerTest.testServer( endpoint.out( From 2efc30768b999426ee394a0a9f23ae6b76cbd0af Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Wed, 5 Feb 2025 15:56:50 +0100 Subject: [PATCH 6/8] Fix conflicting endpoint/server pong handling --- .../scala/sttp/tapir/server/http4s/Http4sServerTest.scala | 1 + .../sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala | 1 + .../sttp/tapir/server/tests/ServerWebSocketTests.scala | 4 +++- .../scala/sttp/tapir/server/vertx/cats/streams/fs2.scala | 6 +++--- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala index d4a7d2bf71..7319aa3813 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -145,6 +145,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi createServerTest, Fs2Streams[IO], autoPing = true, + autoPongAtEndpoint = false, handlePong = false, decodeCloseRequests = false // when a close frame is received, http4s cancels the stream, so sometimes the close frames are never processed diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala index 6c56c60d41..35a7104da7 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala @@ -59,6 +59,7 @@ class ZHttp4sServerTest extends TestSuite with OptionValues { createServerTest, ZioStreams, autoPing = true, + autoPongAtEndpoint = false, handlePong = false, decodeCloseRequests = false // when a close frame is received, http4s cancels the stream, so sometimes the close frames are never processed diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala index 945db195c4..75f6dac5ed 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala @@ -28,6 +28,8 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE]( val streams: S, autoPing: Boolean, handlePong: Boolean, + // some servers (e.g. http4s Ember) can pong on pings automatically without proxying them to endpoint logic + autoPongAtEndpoint: Boolean = true, // Disabled for example for vert.x, which sometimes drops connection without returning Close expectCloseResponse: Boolean = true, frameConcatenation: Boolean = true, @@ -164,7 +166,7 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE]( endpoint.out( webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](streams) .autoPing(None) - .autoPongOnPing(true) + .autoPongOnPing(autoPongAtEndpoint) ), "pong on ping" )((_: Unit) => pureResult(stringEcho.asRight[Unit])) { (backend, baseUri) => diff --git a/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/streams/fs2.scala b/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/streams/fs2.scala index 58f2bd89af..9ed8a8697a 100644 --- a/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/streams/fs2.scala +++ b/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/streams/fs2.scala @@ -47,17 +47,17 @@ object fs2 { _ <- GenSpawn[F].start( stream .evalMap({ chunk => - val buffer = fn(chunk) state.get.flatMap { case StreamState(None, handler, _, _) => - Sync[F].delay(handler.handle(buffer)) + Sync[F].delay(handler.handle(fn(chunk))) case StreamState(Some(promise), _, _, _) => for { _ <- promise.get // Handler in state may be updated since the moment when we wait // promise so let's get more recent version. updatedState <- state.get - } yield updatedState.handler.handle(buffer) + _ <- Sync[F].delay(updatedState.handler.handle(fn(chunk))) + } yield () } }) .onFinalizeCase({ From 51280ad263c1e5bc90f1b98f90fb598a4e2167e7 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Thu, 6 Feb 2025 16:34:52 +0100 Subject: [PATCH 7/8] Flatmap to delayed assertions, not mapping to impure values --- .../server/tests/ServerMultipartTests.scala | 9 +++++---- .../server/tests/ServerWebSocketTests.scala | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerMultipartTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerMultipartTests.scala index 157b27cce3..105299b3c5 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerMultipartTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerMultipartTests.scala @@ -1,6 +1,7 @@ package sttp.tapir.server.tests import cats.implicits._ +import cats.effect.IO import org.scalatest.matchers.should.Matchers._ import sttp.client3.{multipartFile, _} import sttp.model.{Part, StatusCode} @@ -47,14 +48,14 @@ class ServerMultipartTests[F[_], OPTIONS, ROUTE]( .post(uri"$baseUri/api/echo/multipart") .multipartBody(multipart("fruitA", "pineapple".repeat(1100)), multipart("fruitB", "maracuja".repeat(1200))) .send(backend) - .map { r => - r.code shouldBe StatusCode.PayloadTooLarge + .flatMap { r => + IO(r.code shouldBe StatusCode.PayloadTooLarge) } >> basicStringRequest .post(uri"$baseUri/api/echo/multipart") .multipartBody(multipart("fruitA", "pineapple".repeat(850)), multipart("fruitB", "maracuja".repeat(850))) .send(backend) - .map { r => - r.code shouldBe StatusCode.Ok + .flatMap { r => + IO(r.code shouldBe StatusCode.Ok) } } ) diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala index 75f6dac5ed..9281a548bf 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerWebSocketTests.scala @@ -182,13 +182,15 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE]( }) .get(baseUri.scheme("ws")) .send(backend) - .map((r: Response[Either[String, List[WebSocketFrame]]]) => - assert( - r.body.value exists { - case WebSocketFrame.Pong(array) => array sameElements "test-ping-text".getBytes - case _ => false - }, - s"Missing Pong(test-ping-text) in ${r.body}" + .flatMap((r: Response[Either[String, List[WebSocketFrame]]]) => + IO( + assert( + r.body.value exists { + case WebSocketFrame.Pong(array) => array sameElements "test-ping-text".getBytes + case _ => false + }, + s"Missing Pong(test-ping-text) in ${r.body}" + ) ) ) }, @@ -202,7 +204,7 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE]( }) .get(baseUri.scheme("ws")) .send(backend) - .map(r => assert(r.body.forall(_.left.map(_.statusCode) == Left(1000)))) + .flatMap(r => IO(assert(r.body.forall(_.left.map(_.statusCode) == Left(1000))))) }, testServer( endpoint From afa00546dad1bac2c7107c6d3c7091a60aca7bb0 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Wed, 12 Feb 2025 13:57:02 +0100 Subject: [PATCH 8/8] remove unused --- .../scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala | 4 ---- .../server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 87085b12a3..c95fd5e696 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -17,8 +17,6 @@ import sttp.shared.Identity import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter -import scala.concurrent.ExecutionContext - object HelloWorldHttp4sServer extends IOApp: // the endpoint: single fixed path input ("hello"), single query parameter // corresponds to: GET /hello?name=... @@ -29,8 +27,6 @@ object HelloWorldHttp4sServer extends IOApp: val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(helloWorld.serverLogic(name => IO(s"Hello, $name!".asRight[Unit]))) - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - override def run(args: List[String]): IO[ExitCode] = // starting the server EmberServerBuilder diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala index bafbe571c2..329d5a4d15 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala @@ -15,12 +15,11 @@ import sttp.tapir.server.http4s.ztapir.ZHttp4sTestServerInterpreter._ import sttp.tapir.server.tests.TestServerInterpreter import sttp.tapir.tests._ import sttp.tapir.ztapir.ZServerEndpoint -import zio.{Runtime, Task, Unsafe} +import zio.Task import zio.interop._ import zio.interop.catz._ import zio.interop.catz.implicits._ -import scala.concurrent.ExecutionContext import scala.concurrent.duration.{DurationInt, FiniteDuration} object ZHttp4sTestServerInterpreter {