From 8b5ea8a7140f8cca08588250bfa6e2ccb9d8c709 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Sat, 1 Feb 2025 14:24:17 +0100 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 { From bd8513ec03ea040ec4d318680ebb57925a5b5cd1 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Thu, 10 Apr 2025 18:01:29 +0200 Subject: [PATCH 09/20] fix missing Nel --- .../test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala | 1 + 1 file changed, 1 insertion(+) 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 fdeeeb888b..82190e2a69 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 @@ -1,5 +1,6 @@ package sttp.tapir.server.http4s +import cats.data.NonEmptyList import cats.effect._ import cats.effect.unsafe.implicits.global import cats.syntax.all._ From 7b7eda3fd311a4cd2db0855eefabcdd0b459d825 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 28 Jul 2025 12:19:15 +0200 Subject: [PATCH 10/20] fix for changes in upstream --- .../server/http4s/Http4sServerTest.scala | 7 ++----- .../http4s/Http4sTestServerInterpreter.scala | 21 +++++++++++-------- 2 files changed, 14 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 4e4d3b2fea..fe61865781 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 @@ -153,11 +153,8 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi // middleware to add the context to each request (so here string constant) val middleware: ContextMiddleware[IO, String] = ContextMiddleware.const(expectedContext) - BlazeServerBuilder[IO] - .withExecutionContext(ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpWebSocketApp(wsb => middleware(routesWithContext(wsb)).orNotFound) - .resource + interpreter + .buildServer(wsb => middleware(routesWithContext(wsb)).orNotFound) .use { server => val port = server.address.getPort basicRequest 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 422c141866..8248d5dd3d 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 @@ -5,6 +5,7 @@ import cats.effect.{IO, Resource} import cats.syntax.all._ import com.comcast.ip4s import org.http4s.ember.server.EmberServerBuilder +import org.http4s.server.Server import org.http4s.server.websocket.WebSocketBuilder2 import org.http4s.{HttpApp, HttpRoutes} import sttp.capabilities.WebSockets @@ -30,19 +31,21 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I 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 - + def buildServer( + makeService: WebSocketBuilder2[IO] => HttpApp[IO], + gracefulShutdownTimeout: Option[FiniteDuration] = None + ): Resource[IO, Server] = serverBuilder - .withHttpWebSocketApp(service) + .withHttpWebSocketApp(makeService) .withShutdownTimeout( gracefulShutdownTimeout.getOrElse(0.seconds) // no need to wait unless it's explicitly required by test ) .build + + override def server( + routes: NonEmptyList[Routes], + gracefulShutdownTimeout: Option[FiniteDuration] + ): Resource[IO, Port] = + buildServer(wsb => routes.map(_.apply(wsb)).reduceK.orNotFound, gracefulShutdownTimeout) .map(_.address.getPort) - } } From 65945032348284d64e8e8b987c03502990cd75f4 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 28 Jul 2025 14:09:21 +0200 Subject: [PATCH 11/20] try TCP_NODELAY socket option --- .../tapir/server/http4s/Http4sTestServerInterpreter.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 8248d5dd3d..ff6c076ad1 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 @@ -29,7 +29,12 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I } private val anyAvailablePort = ip4s.Port.fromInt(0).get - private val serverBuilder = EmberServerBuilder.default[IO].withPort(anyAvailablePort) + private val serverBuilder = EmberServerBuilder + .default[IO] + .withPort(anyAvailablePort) + .withAdditionalSocketOptions( + List(fs2.io.net.SocketOption.noDelay(true)) // https://github.com/http4s/http4s/issues/7668 + ) def buildServer( makeService: WebSocketBuilder2[IO] => HttpApp[IO], From 29e235e94ac7de3df3bf67f7f23ebdfc707e15c1 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 27 Oct 2025 09:45:21 +0100 Subject: [PATCH 12/20] v0.23.32 --- doc/tutorials/07_cats_effect.md | 4 ++-- .../scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala | 2 +- .../scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala | 2 +- .../scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala | 2 +- .../sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala | 2 +- .../sttp/tapir/examples/client/Http4sClientExample.scala | 2 +- .../tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala | 2 +- .../openapi/MultipleEndpointsDocumentationHttp4sServer.scala | 2 +- .../tapir/examples/openapi/RedocContextPathHttp4sServer.scala | 2 +- .../tapir/examples/security/OAuth2GithubHttp4sServer.scala | 2 +- .../sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala | 2 +- .../tapir/examples/streaming/StreamingHttp4sFs2Server.scala | 2 +- .../examples/streaming/StreamingHttp4sFs2ServerOrError.scala | 2 +- .../sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/tutorials/07_cats_effect.md b/doc/tutorials/07_cats_effect.md index ba84b1be71..bcf90d2aa0 100644 --- a/doc/tutorials/07_cats_effect.md +++ b/doc/tutorials/07_cats_effect.md @@ -132,7 +132,7 @@ 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-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 import cats.effect.{ExitCode, IO, IOApp} import org.http4s.HttpRoutes @@ -194,7 +194,7 @@ 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-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all.* diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 2d2799253a..60c9d0b6a0 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index e15483ca9b..e5845b70be 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 8f048527d5..2d95386fd0 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 27ad50ddb9..058a3ffb23 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -4,7 +4,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> using dep com.softwaremill.sttp.client4::zio:4.0.0-RC3 package sttp.tapir.examples 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 96d3aa135d..5d9d35c964 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.50 //> using dep org.http4s::http4s-circe:0.23.27 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> 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 7648f6cfe7..729f9b98c9 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 package sttp.tapir.examples.errors 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 728a14e00b..9994c25434 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.openapi 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 1fb7611afb..624b531a0c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.openapi 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 487e2fece4..137ef1b2b9 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.50 //> using dep com.softwaremill.sttp.client4::cats:4.0.12 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 //> using dep com.github.jwt-scala::jwt-circe:10.0.1 package sttp.tapir.examples.security 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 27387e9b7d..0d77d6d287 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 //> using dep com.softwaremill.sttp.client4::fs2:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.streaming 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 4d740f4eae..c7cfbc6886 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.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.streaming 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 88aa10a2a9..6673bca7af 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala @@ -2,7 +2,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.50 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.50 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.streaming 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 4bea09dddc..d4cc4c0ea8 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.50 //> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 //> using dep com.softwaremill.sttp.client4::fs2:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 package sttp.tapir.examples.websocket From 5780401f66b90a645ec3735db18b51eae4ca6915 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 16 Feb 2026 15:59:24 +0100 Subject: [PATCH 13/20] fix compilation error, update docs --- generated-doc/out/server/http4s.md | 4 +--- generated-doc/out/server/zio-http4s.md | 12 +++++------- generated-doc/out/tutorials/07_cats_effect.md | 4 ++-- .../sttp/tapir/server/http4s/Http4sServerTest.scala | 3 +-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/generated-doc/out/server/http4s.md b/generated-doc/out/server/http4s.md index 6d9c18b721..3dc5c1fb50 100644 --- a/generated-doc/out/server/http4s.md +++ b/generated-doc/out/server/http4s.md @@ -81,14 +81,12 @@ 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](_ => ???)) - + 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 29c8a95e15..6bcb88a776 100644 --- a/generated-doc/out/server/zio-http4s.md +++ b/generated-doc/out/server/zio-http4s.md @@ -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)) @@ -140,11 +138,11 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = ZHttp4sServerInterpreter().fromWebSocket(wsEndpoint.zServerLogic(_ => ???)).toRoutes val serve: Task[Unit] = - 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/generated-doc/out/tutorials/07_cats_effect.md b/generated-doc/out/tutorials/07_cats_effect.md index d274302ea4..6396827902 100644 --- a/generated-doc/out/tutorials/07_cats_effect.md +++ b/generated-doc/out/tutorials/07_cats_effect.md @@ -132,7 +132,7 @@ standard code to start a server and handle requests until the application is int ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 import cats.effect.{ExitCode, IO, IOApp} import org.http4s.HttpRoutes import org.http4s.ember.server.EmberServerBuilder @@ -193,7 +193,7 @@ the second step that we need to perform: //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.30 +//> using dep org.http4s::http4s-ember-server:0.23.32 import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all.* 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 fe61865781..b2716d1a08 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 @@ -1,6 +1,5 @@ package sttp.tapir.server.http4s -import cats.data.NonEmptyList import cats.effect._ import cats.effect.unsafe.implicits.global import cats.syntax.all._ @@ -40,7 +39,7 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi def assert_get_apiTestRouter_respondsWithExpectedContent[T](routes: HttpRoutes[IO], expectedContext: T): IO[Assertion] = interpreter - .server(NonEmptyList.of(_ => Router("/api" -> routes))) + .server(_ => Router("/api" -> routes)) .use { port => basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend) } From 4c43297049e2c628659c4019f18dccbb6713c80b Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 16 Feb 2026 16:17:41 +0100 Subject: [PATCH 14/20] remove duplicated import --- .../scala/sttp/tapir/server/tests/ServerMultipartTests.scala | 1 - 1 file changed, 1 deletion(-) 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 edec29acc2..18caa02d85 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 @@ -2,7 +2,6 @@ package sttp.tapir.server.tests import cats.effect.IO import cats.implicits._ -import cats.effect.IO import org.scalatest.matchers.should.Matchers._ import org.scalatest.concurrent.Eventually.eventually import sttp.client4.{multipartFile, _} From bd9188fea475b3f5fe3edee95dda8e1a64c72a6b Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 16 Feb 2026 16:45:32 +0100 Subject: [PATCH 15/20] update version in examples --- doc/tutorials/07_cats_effect.md | 4 ++-- .../scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala | 2 +- .../sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala | 2 +- .../scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala | 2 +- .../sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala | 2 +- .../sttp/tapir/examples/client/Http4sClientExample.scala | 6 +++--- .../tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala | 2 +- .../MultipleEndpointsDocumentationHttp4sServer.scala | 2 +- .../examples/openapi/RedocContextPathHttp4sServer.scala | 2 +- .../tapir/examples/security/OAuth2GithubHttp4sServer.scala | 2 +- .../tapir/examples/streaming/ProxyHttp4sFs2Server.scala | 2 +- .../tapir/examples/streaming/StreamingHttp4sFs2Server.scala | 2 +- .../streaming/StreamingHttp4sFs2ServerOrError.scala | 2 +- .../tapir/examples/websocket/WebSocketHttp4sServer.scala | 2 +- generated-doc/out/tutorials/07_cats_effect.md | 4 ++-- 15 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/tutorials/07_cats_effect.md b/doc/tutorials/07_cats_effect.md index bcf90d2aa0..3d4651b99b 100644 --- a/doc/tutorials/07_cats_effect.md +++ b/doc/tutorials/07_cats_effect.md @@ -132,7 +132,7 @@ 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-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 import cats.effect.{ExitCode, IO, IOApp} import org.http4s.HttpRoutes @@ -194,7 +194,7 @@ 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-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all.* diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 69e1ff3054..36aa17bf3e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index bab86672bb..54ed1b9d25 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 9d4fc22616..6ed1eb172c 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 //> using dep dev.zio::zio-interop-cats:23.1.0.3 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index f0c90d7329..efccb1037e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -4,7 +4,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-zio:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 //> using dep com.softwaremill.sttp.client4::zio:4.0.0-RC3 package sttp.tapir.examples 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 dd042fee72..72232fefcd 100644 --- a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -3,9 +3,9 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-client:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.13.8 -//> using dep org.http4s::http4s-circe:0.23.27 -//> using dep org.http4s::http4s-ember-server:0.23.32 -//> using dep org.http4s::http4s-dsl:0.23.27 +//> using dep org.http4s::http4s-circe:0.23.33 +//> using dep org.http4s::http4s-ember-server:0.23.33 +//> using dep org.http4s::http4s-dsl:0.23.33 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 250b69192b..6c24154330 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 package sttp.tapir.examples.errors 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 0e91103018..82238a2da9 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.openapi 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 10ca73e2cf..815edafe9d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.openapi 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 e2e8bf4f11..9f70001466 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.13.8 //> using dep com.softwaremill.sttp.client4::cats:4.0.15 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 //> using dep com.github.jwt-scala::jwt-circe:10.0.1 package sttp.tapir.examples.security 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 7716f1d193..4113010911 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -3,7 +3,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.client4::fs2:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.streaming 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 f1f98bf488..6dc9b03ae4 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.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.client4::core:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.streaming 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 d856e5f845..c945052b48 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala @@ -2,7 +2,7 @@ //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.streaming 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 8be70728c9..38fbd4b119 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.13.8 //> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 //> using dep com.softwaremill.sttp.client4::fs2:4.0.0-RC3 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 package sttp.tapir.examples.websocket diff --git a/generated-doc/out/tutorials/07_cats_effect.md b/generated-doc/out/tutorials/07_cats_effect.md index 6396827902..fbc7e8c27d 100644 --- a/generated-doc/out/tutorials/07_cats_effect.md +++ b/generated-doc/out/tutorials/07_cats_effect.md @@ -132,7 +132,7 @@ standard code to start a server and handle requests until the application is int ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 import cats.effect.{ExitCode, IO, IOApp} import org.http4s.HttpRoutes import org.http4s.ember.server.EmberServerBuilder @@ -193,7 +193,7 @@ the second step that we need to perform: //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.8 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.8 -//> using dep org.http4s::http4s-ember-server:0.23.32 +//> using dep org.http4s::http4s-ember-server:0.23.33 import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all.* From a647f7bda01efc9b79288c353f2dbe2407f8a8a5 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Tue, 17 Feb 2026 03:38:01 +0100 Subject: [PATCH 16/20] fix multipart decoding in http4s --- .../scala/sttp/tapir/server/http4s/Http4sRequestBody.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala index 4d8151d39e..e0cbda5fe5 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala @@ -12,6 +12,7 @@ import sttp.capabilities.fs2.Fs2Streams import sttp.model.{Header, Part} import sttp.tapir.model.ServerRequest import sttp.tapir.server.interpreter.{RawValue, RequestBody} +import sttp.tapir.server.model.InvalidMultipartBodyException import sttp.tapir.{FileRange, InputStreamRange, RawBodyType, RawPart} private[http4s] class Http4sRequestBody[F[_]: Async]( @@ -58,7 +59,7 @@ private[http4s] class Http4sRequestBody[F[_]: Async]( .decode(limitedMedia(http4sRequest(serverRequest), maxBytes), strict = false) .value .flatMap { - case Left(failure) => Sync[F].raiseError(failure) + case Left(failure) => Sync[F].raiseError(InvalidMultipartBodyException(failure)) case Right(mp) => val rawPartsF: Vector[F[RawPart]] = mp.parts .flatMap(part => part.name.flatMap(name => m.partType(name)).map((part, _)).toList) From 7bff9d267c441c926951b4fcc896b1dbd1c97dee Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Tue, 17 Feb 2026 15:40:03 +0100 Subject: [PATCH 17/20] less idle timeouts for zombie clients --- .../src/main/scala/sttp/tapir/client/tests/HttpServer.scala | 3 +++ .../sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala | 1 + .../server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) 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 40dc4ece3f..65900a9536 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 @@ -17,6 +17,8 @@ import org.slf4j.LoggerFactory import org.typelevel.ci.CIString import scodec.bits.ByteVector +import scala.concurrent.duration._ + object HttpServer extends ResourceApp.Forever { private val defaultPort = Port.fromInt(51823).get @@ -232,6 +234,7 @@ class HttpServer(port: Port) { .default[IO] .withPort(port) .withHttpWebSocketApp(app) + .withIdleTimeout(5.seconds) .build .evalTap(_ => IO(logger.info(s"Server on port $port started"))) .onFinalize(IO(logger.info(s"Server on port $port stopped"))) 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 64bd185567..2a2f11fe2d 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 @@ -30,6 +30,7 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I private val serverBuilder = EmberServerBuilder .default[IO] .withPort(anyAvailablePort) + .withIdleTimeout(5.seconds) .withAdditionalSocketOptions( List(fs2.io.net.SocketOption.noDelay(true)) // https://github.com/http4s/http4s/issues/7668 ) 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 0ec44eb124..5ed18ecb79 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 @@ -29,7 +29,7 @@ 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) + private val serverBuilder = EmberServerBuilder.default[Task].withPort(anyAvailablePort).withIdleTimeout(5.seconds) override def route(es: List[ZServerEndpoint[Any, ZioStreams with WebSockets]], interceptors: Interceptors): Routes = { val serverOptions: ServerOptions = interceptors(Http4sServerOptions.customiseInterceptors[Task]).options From 1754699766c8e8c8c3262b78b3e2b7b72f18017a Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Thu, 18 Jun 2026 09:53:29 +0200 Subject: [PATCH 18/20] Use ember's default idle timeout in http4s test interpreters The 5s idle-timeout override cancels in-flight chunked-response writes under load, truncating the response before the terminal chunk and making the client see an EOF mid-read (http4s#6427). This surfaced as a flaky streaming test after the blaze->ember migration. Teardown is handled by withShutdownTimeout(0), so the short timeout is no longer needed for test speed. Also add the noDelay socket option to the ZIO interpreter for parity with the cats one (http4s#7668). Co-Authored-By: Claude Opus 4.8 --- .../server/http4s/Http4sTestServerInterpreter.scala | 5 ++++- .../http4s/ztapir/ZHttp4sTestServerInterpreter.scala | 10 +++++++++- 2 files changed, 13 insertions(+), 2 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 2a2f11fe2d..7444205d31 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 @@ -30,7 +30,10 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I private val serverBuilder = EmberServerBuilder .default[IO] .withPort(anyAvailablePort) - .withIdleTimeout(5.seconds) + // Keep ember's default idle timeout (60s). A short idle timeout cancels in-flight chunked-response + // writes under load, truncating the response before the terminal chunk and causing the client to see + // an EOF mid-read. See https://github.com/http4s/http4s/issues/6427. Teardown is handled by + // withShutdownTimeout(0) below, so the short timeout is not needed for test speed. .withAdditionalSocketOptions( List(fs2.io.net.SocketOption.noDelay(true)) // https://github.com/http4s/http4s/issues/7668 ) 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 5ed18ecb79..813a546c99 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 @@ -29,7 +29,15 @@ 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).withIdleTimeout(5.seconds) + // Keep ember's default idle timeout (60s): a short one cancels in-flight chunked-response writes under + // load, truncating the response and causing the client to see an EOF mid-read (http4s#6427). Teardown is + // handled by withShutdownTimeout(0) below. + private val serverBuilder = EmberServerBuilder + .default[Task] + .withPort(anyAvailablePort) + .withAdditionalSocketOptions( + List(fs2.io.net.SocketOption.noDelay(true)) // https://github.com/http4s/http4s/issues/7668 + ) override def route(es: List[ZServerEndpoint[Any, ZioStreams with WebSockets]], interceptors: Interceptors): Routes = { val serverOptions: ServerOptions = interceptors(Http4sServerOptions.customiseInterceptors[Task]).options From 48c523b89315f5b96ee45f4a6f1325f145c0859a Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 22 Jun 2026 10:39:59 +0200 Subject: [PATCH 19/20] fix typo --- .../scala/sttp/tapir/server/http4s/Http4sServerTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 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 b2716d1a08..4fa8ef5a2e 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 @@ -37,13 +37,13 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val sse1 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) val sse2 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) - def assert_get_apiTestRouter_respondsWithExpectedContent[T](routes: HttpRoutes[IO], expectedContext: T): IO[Assertion] = + def assert_get_apiTestRouter_respondsWithExpectedContent[T](routes: HttpRoutes[IO], expectedContent: T): IO[Assertion] = interpreter .server(_ => Router("/api" -> routes)) .use { port => basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend) } - .map(_.body shouldBe Right(expectedContext)) + .map(_.body shouldBe Right(expectedContent)) def additionalTests(): List[Test] = List( Test("should work with a router and routes in a context") { From e914427bb3c296fb2aef6806ea8e5170331d5b22 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Mon, 22 Jun 2026 10:58:37 +0200 Subject: [PATCH 20/20] small code cleanup --- .../examples/security/OAuth2GithubHttp4sServer.scala | 9 +++------ .../sttp/tapir/server/http4s/Http4sServerTest.scala | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) 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 7e0e0ab4dc..093138e67f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -122,12 +122,9 @@ object OAuth2GithubHttp4sServer extends IOApp: .default[IO] .withHttpApp(Router("/" -> (secretPlaceRoute <+> loginRoute <+> loginGithubRoute(backend))).orNotFound) .build - .use { _ => - IO { - println("Go to: http://localhost:8080") - println("Press any key to exit ...") - scala.io.StdIn.readLine() - } + .evalTap(_ => IO.println("Go to: http://localhost:8080")) + .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 4fa8ef5a2e..814a23bf75 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 @@ -41,9 +41,8 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi interpreter .server(_ => Router("/api" -> routes)) .use { port => - basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend) + basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right(expectedContext)) } - .map(_.body shouldBe Right(expectedContent)) def additionalTests(): List[Test] = List( Test("should work with a router and routes in a context") {