Repository: http4s/http4s
Pull request #7611: Remove code duplication in `RequestId` middleware
Author: login: bio-aeon
Labels: name: module:server
name: module:ember-core
name: series/0.23
description: PRs targeting 0.23.x
Created: 2024-12-30T01:31:24Z | Merged: 2025-04-08T05:53:56Z

PR about cleanup. Looks like the code duplication in `RequestId` middleware can be easily removed (as well as some redundant implicit instances).


Diff:
diff --git a/build.sbt b/build.sbt
index 550d6288aee..c3cbc078e5b 100644
--- a/build.sbt
+++ b/build.sbt
@@ -283,11 +283,6 @@ lazy val tests = libraryCrossProject("tests")
     ),
     githubWorkflowArtifactUpload := false,
   )
-  .nativeSettings(
-    libraryDependencies ++= Seq(
-      epollcat.value
-    )
-  )
   .dependsOn(core, laws)
 
 lazy val server = libraryCrossProject("server")
diff --git a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Client.scala b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Client.scala
index ff6ef72af69..47eaa7a58f2 100644
--- a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Client.scala
+++ b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Client.scala
@@ -372,7 +372,7 @@ private[ember] object H2Client {
           (
             h2.runHttp2Only(req, enableEndpointValidation, enableServerNameIndication) <*
               Resource.eval(socketMap.update(s => s + (key -> Http2)))
-          ).handleErrorWith[org.http4s.Response[F], Throwable] {
+          ).handleErrorWith[org.http4s.Response[F]] {
             case InvalidSocketType() | MissingHost() | MissingPort() =>
               Resource.eval(socketMap.update(s => s + (key -> Http1))) >>
                 http1Client(req)
diff --git a/project/Http4sPlugin.scala b/project/Http4sPlugin.scala
index b62f4d7d011..b585073fda2 100644
--- a/project/Http4sPlugin.scala
+++ b/project/Http4sPlugin.scala
@@ -102,14 +102,13 @@ object Http4sPlugin extends AutoPlugin {
     val blaze = "0.15.3"
     val caseInsensitive = "1.4.2"
     val cats = "2.11.0"
-    val catsEffect = "3.5.7"
+    val catsEffect = "3.6.0"
     val catsParse = "1.0.0"
     val circe = "0.14.8"
     val crypto = "0.2.4"
     val cryptobits = "1.3"
     val disciplineCore = "1.6.0"
-    val epollcat = "0.1.6"
-    val fs2 = "3.11.0"
+    val fs2 = "3.12.0"
     val ip4s = "3.6.0"
     val hpack = "1.0.4"
     val javaWebSocket = "1.6.0"
@@ -158,7 +157,6 @@ object Http4sPlugin extends AutoPlugin {
   lazy val crypto = Def.setting("org.http4s" %%% "http4s-crypto" % V.crypto)
   lazy val cryptobits = "org.reactormonk" %% "cryptobits" % V.cryptobits
   lazy val disciplineCore = Def.setting("org.typelevel" %%% "discipline-core" % V.disciplineCore)
-  lazy val epollcat = Def.setting("com.armanbilge" %%% "epollcat" % V.epollcat)
   lazy val fs2Core = Def.setting("co.fs2" %%% "fs2-core" % V.fs2)
   lazy val fs2Io = Def.setting("co.fs2" %%% "fs2-io" % V.fs2)
   lazy val ip4sCore = Def.setting("com.comcast" %%% "ip4s-core" % V.ip4s)
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 2b70a13b2a9..f3c5f7ca194 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -10,7 +10,7 @@ addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.18.0")
 addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1")
 addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
 addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2")
 addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
 addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
 addSbtPlugin("com.armanbilge" % "sbt-scala-native-config-brew" % "0.3.0")
diff --git a/server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala b/server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
index fbf7bba602f..afebacc633d 100644
--- a/server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
+++ b/server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
@@ -20,9 +20,9 @@ package server
 package middleware
 
 import cats.FlatMap
+import cats.Monad
 import cats.arrow.FunctionK
 import cats.data.Kleisli
-import cats.data.NonEmptyList
 import cats.data.OptionT
 import cats.effect.Sync
 import cats.effect.SyncIO
@@ -44,53 +44,100 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  @deprecated("Preserved for bincompat", "0.23.31")
+  def apply[G[_], F[_]](http: Http[G, F], G: Sync[G]): Http[G, F] =
+    apply(requestIdHeader)(http)(G, UUIDGen.fromSync(G))
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      fk: F ~> G,
+      headerName: CIString,
+      genReqId: F[UUID],
+      http: Http[G, F],
+      G: FlatMap[G],
+      F: Sync[F],
+  ): Http[G, F] = {
+    implicit val monadForG: Monad[G] = new Monad[G] {
+      def pure[A](a: A): G[A] = fk(F.pure(a))
+
+      def flatMap[A, B](fa: G[A])(f: A => G[B]): G[B] = G.flatMap(fa)(f)
+
+      def tailRecM[A, B](a: A)(f: A => G[Either[A, B]]): G[B] =
+        G.tailRecM(a)(f)
+    }
+
+    apply(headerName, fk(genReqId))(http)
+  }
+
+  def apply[G[_]: Monad, F[_]](
       fk: F ~> G,
       headerName: CIString = requestIdHeader,
       genReqId: F[UUID],
-  )(http: Http[G, F])(implicit G: FlatMap[G], F: Sync[F]): Http[G, F] =
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, fk(genReqId))(http)
+
+  def apply[G[_], F[_]](headerName: CIString, genReqId: G[UUID])(
+      http: Http[G, F]
+  )(implicit G: Monad[G]): Http[G, F] =
     Kleisli[G, Request[F], Response[F]] { req =>
       for {
-        header <- fk(req.headers.get(headerName) match {
-          case None => genReqId.map(reqId => Header.Raw(headerName, reqId.show))
-          case Some(NonEmptyList(header, _)) => F.pure(header)
-        })
+        header <- req.headers
+          .get(headerName)
+          .map(_.head)
+          .fold(genReqId.map(reqId => Header.Raw(headerName, reqId.show)))(G.pure)
         reqId = header.value
         response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
       } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
     }
 
   object httpApp {
-    def apply[F[_]: Sync](httpApp: HttpApp[F]): HttpApp[F] =
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](httpApp: HttpApp[F], F: Sync[F]): HttpApp[F] =
+      apply(httpApp)(F, UUIDGen.fromSync(F))
+
+    def apply[F[_]: Monad: UUIDGen](httpApp: HttpApp[F]): HttpApp[F] =
       RequestId.apply(requestIdHeader)(httpApp)
 
-    def apply[F[_]: Sync](
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](headerName: CIString, httpApp: HttpApp[F], F: Sync[F]): HttpApp[F] =
+      apply(headerName)(httpApp)(F, UUIDGen.fromSync(F))
+
+    def apply[F[_]: Monad: UUIDGen](
         headerName: CIString
     )(httpApp: HttpApp[F]): HttpApp[F] =
       RequestId.apply(headerName)(httpApp)
 
-    def apply[F[_]: Sync](
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](
+        headerName: CIString,
+        genReqId: F[UUID],
+        httpApp: HttpApp[F],
+        F: Sync[F],
+    ): HttpApp[F] =
+      apply(headerName, genReqId)(httpApp)(F)
+
+    def apply[F[_]: Monad](
         headerName: CIString = requestIdHeader,
         genReqId: F[UUID],
     )(httpApp: HttpApp[F]): HttpApp[F] =
@@ -98,15 +145,32 @@ object RequestId {
   }
 
   object httpRoutes {
-    def apply[F[_]: Sync](httpRoutes: HttpRoutes[F]): HttpRoutes[F] =
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](httpRoutes: HttpRoutes[F], F: Sync[F]): HttpRoutes[F] =
+      apply(httpRoutes)(F, UUIDGen.fromSync(F))
+
+    def apply[F[_]: Monad: UUIDGen](httpRoutes: HttpRoutes[F]): HttpRoutes[F] =
       RequestId.apply(requestIdHeader)(httpRoutes)
 
-    def apply[F[_]: Sync](
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](headerName: CIString, httpRoutes: HttpRoutes[F], F: Sync[F]): HttpRoutes[F] =
+      apply(headerName)(httpRoutes)(F, UUIDGen.fromSync(F))
+
+    def apply[F[_]: Monad: UUIDGen](
         headerName: CIString
     )(httpRoutes: HttpRoutes[F]): HttpRoutes[F] =
       RequestId.apply(headerName)(httpRoutes)
 
-    def apply[F[_]: Sync](
+    @deprecated("Preserved for bincompat", "0.23.31")
+    def apply[F[_]](
+        headerName: CIString,
+        genReqId: F[UUID],
+        httpRoutes: HttpRoutes[F],
+        F: Sync[F],
+    ): HttpRoutes[F] =
+      apply(headerName, genReqId)(httpRoutes)(F)
+
+    def apply[F[_]: Monad](
         headerName: CIString = requestIdHeader,
         genReqId: F[UUID],
     )(httpRoutes: HttpRoutes[F]): HttpRoutes[F] =
diff --git a/tests/native/src/main/scala/org/http4s/Http4sSuitePlatform.scala b/tests/native/src/main/scala/org/http4s/Http4sSuitePlatform.scala
index 2474fdd8993..b226363d997 100644
--- a/tests/native/src/main/scala/org/http4s/Http4sSuitePlatform.scala
+++ b/tests/native/src/main/scala/org/http4s/Http4sSuitePlatform.scala
@@ -16,12 +16,7 @@
 
 package org.http4s
 
-import cats.effect.unsafe.IORuntime
-import epollcat.unsafe.EpollRuntime
-
 trait Http4sSuitePlatform { this: Http4sSuite =>
-  override def munitIORuntime: IORuntime = EpollRuntime.global
-
   // allow flaky tests on ci
   override def munitFlakyOK: Boolean = sys.env.contains("CI")
 }


Reviews:
user: login: armanbilge
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-30T22:00:46Z
user: login: bio-aeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-30T22:11:02Z
user: login: armanbilge
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T13:52:48Z
user: login: danicheg
body: Overall LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T14:48:29Z
user: login: bio-aeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T16:21:11Z
user: login: armanbilge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T05:53:34Z

Review comments:
diff_hunk: @@ -44,53 +45,104 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  def apply[G[_]: Sync, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.30")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.30")
path: server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
user: login: armanbilge
body: v0.23.30 was already released.

```suggestion
  @deprecated("Preserved for bincompat", "0.23.31")
```
created_at: 2024-12-30T22:00:46Z
author_association: MEMBER
diff_hunk: @@ -44,53 +45,104 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  def apply[G[_]: Sync, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.30")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.30")
path: server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
user: login: bio-aeon
body: Updated.
created_at: 2024-12-30T22:11:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,69 +44,133 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  @deprecated("Preserved for bincompat", "0.23.31")
+  def apply[G[_], F[_]](http: Http[G, F], G: Sync[G]): Http[G, F] =
+    apply(requestIdHeader)(http)(G, UUIDGen.fromSync(G))
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      fk: F ~> G,
+      headerName: CIString,
+      genReqId: F[UUID],
+      http: Http[G, F],
+      G: FlatMap[G],
+      F: Sync[F],
+  ): Http[G, F] = {
+    implicit val monadForG: Monad[G] = new Monad[G] {
+      def pure[A](a: A): G[A] = fk(F.pure(a))
+
+      def flatMap[A, B](fa: G[A])(f: A => G[B]): G[B] = G.flatMap(fa)(f)
+
+      def tailRecM[A, B](a: A)(f: A => G[Either[A, B]]): G[B] =
+        G.tailRecM(a)(f)
+    }
path: server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
user: login: danicheg
body: I could barely be called a category theorist, but this Monad instance might break the left-identity and right-identity laws, as `F ~> G` might not preserve the monadic structure. For example, consider `F = Id` and `G = Option`, where `FunctionK[F, G]#apply` returns `None`. That case might seem degenerate, but it illustrates the basic idea. Should we worry about this? I dunno! Leaving it for you to judge.
created_at: 2025-04-03T14:45:20Z
author_association: MEMBER
diff_hunk: @@ -44,69 +44,133 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  @deprecated("Preserved for bincompat", "0.23.31")
+  def apply[G[_], F[_]](http: Http[G, F], G: Sync[G]): Http[G, F] =
+    apply(requestIdHeader)(http)(G, UUIDGen.fromSync(G))
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      fk: F ~> G,
+      headerName: CIString,
+      genReqId: F[UUID],
+      http: Http[G, F],
+      G: FlatMap[G],
+      F: Sync[F],
+  ): Http[G, F] = {
+    implicit val monadForG: Monad[G] = new Monad[G] {
+      def pure[A](a: A): G[A] = fk(F.pure(a))
+
+      def flatMap[A, B](fa: G[A])(f: A => G[B]): G[B] = G.flatMap(fa)(f)
+
+      def tailRecM[A, B](a: A)(f: A => G[Either[A, B]]): G[B] =
+        G.tailRecM(a)(f)
+    }
path: server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
user: login: bio-aeon
body: I think this is some kind of trade-off for deprecation of the old method and not copy-pasting the same code several times. Unfortunately, it doesn't look like we have other good alternatives.
created_at: 2025-04-05T16:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,69 +44,133 @@ object RequestId {
 
   val requestIdAttrKey: Key[String] = Key.newKey[SyncIO, String].unsafeRunSync()
 
-  def apply[G[_], F[_]](http: Http[G, F])(implicit G: Sync[G]): Http[G, F] =
+  @deprecated("Preserved for bincompat", "0.23.31")
+  def apply[G[_], F[_]](http: Http[G, F], G: Sync[G]): Http[G, F] =
+    apply(requestIdHeader)(http)(G, UUIDGen.fromSync(G))
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](http: Http[G, F]): Http[G, F] =
     apply(requestIdHeader)(http)
 
   @deprecated("Preserved for bincompat", "0.23.12")
   def apply[G[_], F[_]](headerName: CIString, http: Http[G, F], G: Sync[G]): Http[G, F] =
     apply(headerName)(http)(G, UUIDGen.fromSync(G))
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      headerName: CIString,
+      http: Http[G, F],
+      G: Sync[G],
+      gen: UUIDGen[G],
+  ): Http[G, F] =
+    apply(headerName)(http)(G, gen)
+
+  def apply[G[_]: Monad: UUIDGen, F[_]](
       headerName: CIString
-  )(http: Http[G, F])(implicit G: Sync[G], gen: UUIDGen[G]): Http[G, F] =
-    Kleisli[G, Request[F], Response[F]] { req =>
-      for {
-        header <- req.headers.get(headerName).map(_.head) match {
-          case None => UUIDGen.randomString[G].map(Header.Raw(headerName, _))
-          case Some(header) => G.pure(header)
-        }
-        reqId = header.value
-        response <- http(req.withAttribute(requestIdAttrKey, reqId).putHeaders(header))
-      } yield response.withAttribute(requestIdAttrKey, reqId).putHeaders(header)
-    }
+  )(http: Http[G, F]): Http[G, F] =
+    apply(headerName, UUIDGen.randomUUID[G])(http)
 
+  @deprecated("Preserved for bincompat", "0.23.31")
   def apply[G[_], F[_]](
+      fk: F ~> G,
+      headerName: CIString,
+      genReqId: F[UUID],
+      http: Http[G, F],
+      G: FlatMap[G],
+      F: Sync[F],
+  ): Http[G, F] = {
+    implicit val monadForG: Monad[G] = new Monad[G] {
+      def pure[A](a: A): G[A] = fk(F.pure(a))
+
+      def flatMap[A, B](fa: G[A])(f: A => G[B]): G[B] = G.flatMap(fa)(f)
+
+      def tailRecM[A, B](a: A)(f: A => G[Either[A, B]]): G[B] =
+        G.tailRecM(a)(f)
+    }
path: server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala
user: login: armanbilge
body: Good catch, that's a fair point. I suspect the answer here is that there are some (currently undocumented) pre-conditions on what kind of `F ~> G` you can pass to these builders, and those pre-conditions would enable you to prove that you can derive a lawful `Monad` in this way.
created_at: 2025-04-08T05:53:33Z
author_association: MEMBER

PR comments:
user: login: armanbilge
created_at: 2024-12-30T18:11:13Z
body: Thanks for the PR! This is actually very helpful, due to some upcoming changes in Cats Effect v3.6.0. If you don't mind, I have a couple suggestions/requests.

1. Do you mind opening a PR against series/0.23.x? I can help you make the changes binary-compatibly.
2. Can we deprecate all signatures that ask for `Sync`? We should replace them with signatures that ask for an implicit `UUIDGen`. This change is needed to upgrade to Cats Effect v3.6.0.

To make changes binary-compatibly, you just need to follow the existing technique already used in this file:
https://github.com/http4s/http4s/blob/ba16899cecb15f8bb990984b8470d188cfd00f6c/server/shared/src/main/scala/org/http4s/server/middleware/RequestId.scala#L50-L52

Let me know if you have any questions!
author_association: MEMBER
user: login: bio-aeon
created_at: 2024-12-30T20:23:55Z
body: @armanbilge Thank you for the feedback

1. I don't mind having PR against `series/0.23.x` branch, but in this case I don't see an easy way to get rid of duplicated code in one of deprecated methods, because it has only `FlatMap[G]` instance, while we also need `Applicative[G]` for the newly introduced `apply`. If it doesn't matter, then I don't see any problems. By the way, I force-pushed to change the base branch, sorry about that, subsequent updates will be in separate commits. I rewrote the PR using deprecation technique you mentioned.
2. For deprecating all signatures with `Sync` and replacing them by using `Monad` + `UUIDGen`, in a couple of cases we need some way to derive `UUIDGen[OptionT[F, *]]`, like it's being done through `syncForOptionT` for `Sync` now. What do you think about creating a new implicit instance for this right inside `http4s`?
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2024-12-30T20:56:37Z
body: > 1. but in this case I don't see an easy way to get rid of duplicated code in one of deprecated methods, because it has only `FlatMap[G]` instance, while we also need `Applicative[G]` for the newly introduced `apply`. If it doesn't matter, then I don't see any problems.

Oh, that's unfortunate. Let me take a closer look and see if I have any ideas.

---

> 2\. in a couple of cases we need some way to derive `UUIDGen[OptionT[F, *]]`

Oops, I didn't realize these are missing. Do you mind opening a PR to Cats Effect to add them? Here's an example of how we do it for `Random`.

https://github.com/typelevel/cats-effect/blob/0cb794be902b39d0e43ae0ad3a44e10c94edabff/std/shared/src/main/scala/cats/effect/std/Random.scala#L233-L234
author_association: MEMBER
user: login: bio-aeon
created_at: 2024-12-30T22:13:19Z
body: > Oops, I didn't realize these are missing. Do you mind opening a PR to Cats Effect to add them?

Yes, sure https://github.com/typelevel/cats-effect/pull/4220
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2024-12-30T23:41:46Z
body: > 1. because it has only `FlatMap[G]` instance, while we also need `Applicative[G]`

If you have `pure` for `F[_]` and you have `F ~> G`, then you have `pure` for `G[_]`.
author_association: MEMBER
user: login: bio-aeon
created_at: 2024-12-31T00:50:30Z
body: > If you have pure for `F[_]` and you have `F ~> G`, then you have `pure` for `G[_]`.

Yes, that's true, but it's still not entirely clear how to apply this without workarounds.
What the PR's idea looked like initially. It was to add the new `apply` method which completely generalizes the logic of the two existing `apply` methods and has the signature:
```scala
def apply[G[_], F[_]](headerName: CIString, genReqId: G[UUID])(
    http: Http[G, F]
)(implicit G: Monad[G]): Http[G, F]
```
Then express existing `apply` methods through it like:
```scala
def apply[G[_]: Monad: UUIDGen, F[_]](
    headerName: CIString
)(http: Http[G, F]): Http[G, F] =
  apply(headerName, UUIDGen.randomUUID[G])(http)
...
def apply[G[_]: Monad, F[_]](
    fk: F ~> G,
    headerName: CIString = requestIdHeader,
    genReqId: F[UUID],
)(http: Http[G, F]): Http[G, F] =
  apply(headerName, fk(genReqId))(http)
```
And that's it.

With the deprecated method, there are two "workaround" options, other than leaving everything "as-is" and delete sometime later.
1. Somehow construct a `Monad[G]` instance "in-place", having `Sync[F]` and `F ~> G`.
2. Change the new `apply` method's signature so that instead of `Monad[G]` it would require the explicit `pure: A => G[A]` and the implicit `FlatMap[G]`.

Both of these options don't look graceful to me. But maybe there is something else and I just didn't fully understand your idea.
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2024-12-31T01:01:55Z
body: > 2\. Change the new `apply` method's signature so that instead of `Monad[G]` it would require the explicit `pure: A => G[A]` and the implicit `FlatMap[G]`.

`pure` + `flatMap` _is_ a `Monad` :) you don't need to change the method signature, you can just do `new Monad[G] ...` and implement the methods.
author_association: MEMBER
user: login: bio-aeon
created_at: 2024-12-31T01:33:34Z
body: > `pure` + `flatMap` _is_ a `Monad` :) you don't need to change the method signature, you can just do `new Monad[G] ...` and implement the methods.

Yes, that's actually what I meant by `construct a Monad[G] instance "in-place"` in option _1_. Still looks like a bit of a hack to me, but maybe the lesser evil :)
Pushed changes with this way.
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2024-12-31T01:40:24Z
body: > Still looks like a bit of a hack to me, but maybe the lesser evil :)

If it makes you feel better, we have deployed similar hacks elsewhere 😅 

https://github.com/http4s/http4s/blob/3f9c97813961c61d01f3db77a4ef0d699d826894/client/shared/src/main/scala/org/http4s/client/Client.scala#L229-L231

I think it's a reasonable way to de-duplicate the implementations of deprecated methods. It will eventually go away.

In my opinion, slapping on a `Sync` constraint instead of the precise constraints needed is the worst hack of all 😁 

Thanks for your work here and in CE 🙂 
author_association: MEMBER
user: login: bio-aeon
created_at: 2024-12-31T02:02:12Z
body: > If it makes you feel better, we have deployed similar hacks elsewhere 😅

Yeah, feels much better now 😄

> Thanks for your work here and in CE 🙂

Thanks for your comments. Looking forward for CE release to complete this PR.
author_association: CONTRIBUTOR
user: login: samspills
created_at: 2025-03-01T03:37:57Z
body: Thanks so much for your work! 
> Looking forward for CE release to complete this PR.

I'm just going to pop this PR into a draft state while we wait on the final 3.6.0 CE release
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2025-03-26T15:59:49Z
body: @bio-aeon CE v3.6.0 was finally released, if you have time to finish up this PR!
author_association: MEMBER
user: login: bio-aeon
created_at: 2025-03-26T21:00:43Z
body: @armanbilge I updated CE to 3.6.0 and deprecated all `RequestId` signatures containing `Sync` in favor of `Monad` + `UUIDGen`.
Not really sure, why does `org.http4s.ember.server.ConnectionSuite` fail in case of `rootNative`, while it succeeds locally on my laptop.
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2025-03-29T01:03:29Z
body: > while it succeeds locally on my laptop.

What operating system are you using? It might be an OS-specific issue 😕 
author_association: MEMBER
user: login: bio-aeon
created_at: 2025-03-29T03:58:30Z
body: I use Ubuntu 24.04.
Any ideas, why this specific test's behavior is different exactly for `rootNative`?
author_association: CONTRIBUTOR
user: login: armanbilge
created_at: 2025-04-01T23:57:21Z
body: > Any ideas, why this specific test's behavior is different exactly for `rootNative`?

Yes — we have a custom networking layer for Scala Native, and Cats Effect v3.6.0 introduced significant changes to it.
author_association: MEMBER
user: login: danicheg
created_at: 2025-04-02T09:04:05Z
body: FWIW, it'd be nice to separate changes related to the upgrade of CE and refactoring of the `RequestId` middleware. 🙏🏻 
author_association: MEMBER
user: login: armanbilge
created_at: 2025-04-02T17:02:11Z
body: > FWIW, it'd be nice to separate changes related to the upgrade of CE and refactoring of the `RequestId` middleware. 🙏🏻

@danicheg I agree, I'm not sure if that's possible. CE v3.6.0 deprecated the `UUIDGen` constructors that `RequestId` was using, which required the `RequestId` refactor (coincidentally, @bio-aeon had already started working on that anyway). So now I think we have to merge these changes/updates all at the same time. wdyt?
author_association: MEMBER