Repository: http4s/http4s (scala)

Issue:
Ember server hangs on HTTP/2 request with body
This is a regression between v0.23.19-RC3 and v0.23.19 that I bisected to https://github.com/http4s/http4s/pull/7096.

https://gist.github.com/Baccata/f29f5154b2628b047dd4d41b5c7725bc

<details>

```scala
//> using lib "org.http4s::http4s-ember-server:0.23.19"
//> using lib "org.http4s::http4s-dsl:0.23.19"

package foo

import cats.effect._
import cats.syntax.all._
import org.http4s.implicits._
import org.http4s.ember.server._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.dsl.request

object Main extends IOApp.Simple {

  // this works (http2 request w/o body):
  //    curl --http2 localhost:8080/hello
  // this works (http1 request w/ body):
  //    curl -X POST -d foo localhost:8080/text
  // this doesn't (http2 request w/ body, server hangs)
  //    curl --http2 -X POST -d foo localhost:8080/text
  //
  // Additionally, impossible to exit the application with a ctrl-c as soon
  // as ANY request has been sent to the server (whether we hit the first path or not)
  val app = HttpApp[IO] {
    case GET -> Root / "hello"           => Ok("hello")
    case request @ POST -> Root / "text" => request.as[String].flatMap(Ok(_))
    case request                         => NotFound()
  }

  def run: IO[Unit] = EmberServerBuilder
    .default[IO]
    .withHttp2
    .withHttpApp(app)
    .build
    .useForever
    .void

}
```

</details>

I was also able to reproduce with the `--http2-prior-knowledge` flag.

Pull request description:
Fixes https://github.com/http4s/http4s/issues/7146. The `Channel` introduced in https://github.com/http4s/http4s/pull/7096 was not being closed in the right place.

Patch:
diff --git a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Connection.scala b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Connection.scala
index ba83bdab782..4a6b97d9c51 100644
--- a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Connection.scala
+++ b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Connection.scala
@@ -92,7 +92,7 @@ private[h2] class H2Connection[F[_]](
       refState,
       hpack,
       outgoing,
-      body.close *> closedStreams.offer(id),
+      closedStreams.offer(id),
       goAway,
       logger,
     )
@@ -128,7 +128,7 @@ private[h2] class H2Connection[F[_]](
       refState,
       hpack,
       outgoing,
-      body.close *> closedStreams.offer(id),
+      closedStreams.offer(id),
       goAway,
       logger,
     )
diff --git a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Stream.scala b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Stream.scala
index 5bdd989456d..f323f4e0ec1 100644
--- a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Stream.scala
+++ b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/H2Stream.scala
@@ -339,7 +339,9 @@ private[h2] class H2Stream[F[_]: Concurrent](
             if (needsWindowUpdate && !isClosed && sizeReadOk) {
               enqueue.offer(Chunk.singleton(H2Frame.WindowUpdate(id, windowSize - newSize)))
             } else Applicative[F].unit
-          _ <- if (data.endStream) s.trailWith(List.empty).void else Applicative[F].unit
+          _ <-
+            if (data.endStream) s.readBuffer.close *> s.trailWith(List.empty).void
+            else Applicative[F].unit
           _ <-
             if (isClosed && sizeReadOk) onClosed else Applicative[F].unit
         } yield ()
diff --git a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/PseudoHeaders.scala b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/PseudoHeaders.scala
index cf111599641..7c564e5a965 100644
--- a/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/PseudoHeaders.scala
+++ b/ember-core/shared/src/main/scala/org/http4s/ember/core/h2/PseudoHeaders.scala
@@ -106,16 +106,7 @@ private[h2] object PseudoHeaders {
   def extractAuthority(headers: List[(String, String)]): Option[Uri.Authority] =
     headers.collectFirstSome {
       case (PseudoHeaders.AUTHORITY, value) =>
-        val index = value.indexOf(":")
-        if (index > 0 && index < value.length) {
-          Option(
-            Uri.Authority(
-              userInfo = None,
-              host = Uri.RegName(value.take(index)),
-              port = value.drop(index + 1).toInt.some,
-            )
-          )
-        } else Option.empty
+        Uri.fromString(value).toOption.flatMap(_.authority)
       case (_, _) => None
     }
 


Test patch:
diff --git a/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala b/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
index 565f9d96240..0b12419ea1d 100644
--- a/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
+++ b/ember-server/shared/src/test/scala/org/http4s/ember/server/EmberServerSuite.scala
@@ -25,6 +25,7 @@ import fs2.io.net.ConnectException
 import org.http4s._
 import org.http4s.ember.client.EmberClientBuilder
 import org.http4s.ember.core.EmberException
+import org.http4s.ember.core.h2.H2Keys.Http2PriorKnowledge
 import org.http4s.implicits._
 import org.http4s.server.Server
 
@@ -154,4 +155,25 @@ class EmberServerSuite extends Http4sSuite {
       }
   }
 
+  test("#7146 - HTTP/2 request with body") {
+    val server = EmberServerBuilder
+      .default[IO]
+      .withPort(port"0")
+      .withHttp2
+      .withHttpApp(service)
+      .build
+
+    val client = EmberClientBuilder
+      .default[IO]
+      .withHttp2
+      .build
+
+    (server, client).tupled.use { case (server, client) =>
+      val req = Request[IO](Method.POST, uri = url(server.addressIp4s, "/echo"))
+        .withEntity("hello")
+        .withAttribute(Http2PriorKnowledge, ())
+      client.expect[String](req).assertEquals("hello")
+    }
+  }
+
 }