Repository: http4s/http4s
Pull request #6787 (2022-11-03 19:01:02)

Merge `series/0.23` into `main`


Patch:
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 5d1467b96a..2fe5bb6e66 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,4 +1,4 @@
-version = 3.5.9
+version = 3.6.1
 
 style = default
 
diff --git a/build.sbt b/build.sbt
index dc436131ca..71463e103c 100644
--- a/build.sbt
+++ b/build.sbt
@@ -545,17 +545,7 @@ def http4sCrossProject(name: String, crossType: CrossType) =
     .nativeSettings(
       tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.23.16").toMap,
       unusedCompileDependenciesTest := {},
-      nativeConfig ~= { c =>
-        Option(System.getenv("DEVSHELL_DIR")).fold(c) { devshellDir =>
-          c.withCompileOptions(c.compileOptions :+ s"-I$devshellDir/include")
-            .withLinkingOptions(c.linkingOptions :+ s"-L$devshellDir/lib")
-        }
-      },
-      Test / envVars ++= {
-        val ldLibPath = Option(System.getenv("DEVSHELL_DIR"))
-          .map(devshellDir => "LD_LIBRARY_PATH" -> s"$devshellDir/lib")
-        Map("S2N_DONT_MLOCK" -> "1") ++ ldLibPath.toMap
-      },
+      Test / envVars += "S2N_DONT_MLOCK" -> "1",
     )
     .enablePlugins(Http4sPlugin)
     .configurePlatforms(JSPlatform, NativePlatform)(_.disablePlugins(DoctestPlugin))
diff --git a/circe/src/main/scala/org/http4s/circe/DecodingFailures.scala b/circe/src/main/scala/org/http4s/circe/DecodingFailures.scala
index d9c60f5eea..ee38851093 100644
--- a/circe/src/main/scala/org/http4s/circe/DecodingFailures.scala
+++ b/circe/src/main/scala/org/http4s/circe/DecodingFailures.scala
@@ -24,5 +24,5 @@ import io.circe.DecodingFailure
   * [[accumulatingJsonOf]] to decode JSON messages.
   */
 final case class DecodingFailures(failures: NonEmptyList[DecodingFailure]) extends Exception {
-  override def getMessage: String = failures.toList.map(_.show).mkString("\n")
+  override def getMessage: String = failures.iterator.map(_.show).mkString("\n")
 }
diff --git a/client/shared/src/main/scala/org/http4s/client/middleware/Retry.scala b/client/shared/src/main/scala/org/http4s/client/middleware/Retry.scala
index 4c430e50eb..660d5f7e49 100644
--- a/client/shared/src/main/scala/org/http4s/client/middleware/Retry.scala
+++ b/client/shared/src/main/scala/org/http4s/client/middleware/Retry.scala
@@ -58,7 +58,7 @@ object Retry {
       logRetries: Boolean = true,
   )(client: Client[F])(implicit F: Temporal[F]): Client[F] = {
     def showRequest(request: Request[F], redactWhen: CIString => Boolean): String = {
-      val headers = request.headers.redactSensitive(redactWhen).headers.mkString(",")
+      val headers = request.headers.mkString(",", redactWhen)
       val uri = request.uri.renderString
       val method = request.method
       s"method=$method uri=$uri headers=$headers"
diff --git a/client/shared/src/main/scala/org/http4s/client/oauth1/oauth1.scala b/client/shared/src/main/scala/org/http4s/client/oauth1/oauth1.scala
index 025b24ecc7..6cc38b5119 100644
--- a/client/shared/src/main/scala/org/http4s/client/oauth1/oauth1.scala
+++ b/client/shared/src/main/scala/org/http4s/client/oauth1/oauth1.scala
@@ -33,7 +33,6 @@ import org.http4s.client.oauth1.ProtocolParameter.Timestamp
 import org.http4s.client.oauth1.ProtocolParameter.Verifier
 import org.http4s.client.oauth1.ProtocolParameter.Version
 import org.http4s.headers.Authorization
-import org.typelevel.ci._
 
 import scala.collection.immutable
 import scala.collection.mutable.ListBuffer
@@ -125,12 +124,12 @@ package object oauth1 {
       val baseStr = mkBaseString(
         method,
         uri,
-        (headers ++ queryParams).sorted.map(Show[ProtocolParameter].show).mkString("&"),
+        (headers ++ queryParams).sorted.iterator.map(Show[ProtocolParameter].show).mkString("&"),
       )
       val alg = SignatureAlgorithm.unsafeFromMethod(signatureMethod)
       makeSHASig[F](baseStr, consumer.secret, token.map(_.secret), alg).map { sig =>
         val creds = Credentials.AuthParams(
-          ci"OAuth",
+          AuthScheme.OAuth,
           NonEmptyList(
             "oauth_signature" -> encode(sig),
             realm.fold(headers.map(_.toTuple))(_.toTuple +: headers.map(_.toTuple)).toList,
@@ -177,7 +176,10 @@ package object oauth1 {
     )
     makeSHASig(baseString, consumer.secret, token.map(_.secret), algorithm).map { sig =>
       val creds =
-        Credentials.AuthParams(ci"OAuth", NonEmptyList("oauth_signature" -> encode(sig), params))
+        Credentials.AuthParams(
+          AuthScheme.OAuth,
+          NonEmptyList("oauth_signature" -> encode(sig), params),
+        )
 
       Authorization(creds)
     }
diff --git a/core/shared/src/main/scala/org/http4s/Header.scala b/core/shared/src/main/scala/org/http4s/Header.scala
index 5ed577afed..93cbde8960 100644
--- a/core/shared/src/main/scala/org/http4s/Header.scala
+++ b/core/shared/src/main/scala/org/http4s/Header.scala
@@ -55,7 +55,7 @@ trait Header[A, T <: Header.Type] {
 
 object Header {
   final case class Raw(name: CIString, value: String) {
-    override def toString: String = s"${name}: ${value}"
+    override def toString: String = Raw.toString(name, value)
 
     /** True if [[name]] is a valid field-name per RFC7230.  Where it
       * is not, the header may be dropped by the backend.
@@ -70,6 +70,9 @@ object Header {
   }
 
   object Raw {
+    @inline private[http4s] def toString(name: CIString, value: String): String =
+      s"${name.toString}: $value"
+
     implicit lazy val catsInstancesForHttp4sHeaderRaw
         : Order[Raw] with Hash[Raw] with Show[Raw] with Renderer[Raw] = new Order[Raw]
       with Hash[Raw]
@@ -128,7 +131,7 @@ object Header {
     * headers and key-value pairs.
     *
     * A method taking variadic `ToRaw` arguments will allow taking
-    * heteregenous arguments, provided they are either:
+    * heterogeneous arguments, provided they are either:
     *
     * - A value of type `A`  which has a `Header[A]` in scope
     * - A (name, value) pair of `String`, which is treated as a `Recurring` header
diff --git a/core/shared/src/main/scala/org/http4s/Headers.scala b/core/shared/src/main/scala/org/http4s/Headers.scala
index 301cde93ab..b2f0c4e31d 100644
--- a/core/shared/src/main/scala/org/http4s/Headers.scala
+++ b/core/shared/src/main/scala/org/http4s/Headers.scala
@@ -153,6 +153,54 @@ final class Headers(val headers: List[Header.Raw]) extends AnyVal {
 
   def foreach(f: Header.Raw => Unit): Unit = headers.foreach(f)
 
+  /** Creates a string representation for a list of headers
+    * and redacts sensitive headers' values.
+    *
+    *  @param start       the starting string
+    *  @param separator   the separator string
+    *  @param end         the ending string
+    *  @param redactWhen  the function for filtering out header values of sensitive headers
+    *  @return            a string representation of the list of headers.
+    *                      The resulting string begins with the string `start`
+    *                      and ends with the string `end`. Inside, the string
+    *                      representations of all headers are separated
+    *                      by the string `separator`. Sensitive headers' values
+    *                      are redacted with the `redactWhen` function.
+    */
+  def mkString(
+      start: String,
+      separator: String,
+      end: String,
+      redactWhen: CIString => Boolean,
+  ): String =
+    headers.iterator
+      .map {
+        case h if redactWhen(h.name) => Header.Raw.toString(h.name, "<REDACTED>")
+        case h => Header.Raw.toString(h.name, h.value)
+      }
+      .mkString(start, separator, end)
+
+  /** Creates a string representation for a list of headers
+    * and redacts sensitive headers' values.
+    *
+    *  @param separator   the separator string
+    *  @param redactWhen  the function for filtering out header values of sensitive headers
+    *  @return            a string representation of the list of headers.
+    *                      The resulting string is constructed from the string representations
+    *                      of all headers separated by the string `separator`. Sensitive headers'
+    *                      values are redacted with the `redactWhen` function.
+    */
+  def mkString(
+      separator: String,
+      redactWhen: CIString => Boolean,
+  ): String =
+    headers.iterator
+      .map {
+        case h if redactWhen(h.name) => Header.Raw.toString(h.name, "<REDACTED>")
+        case h => Header.Raw.toString(h.name, h.value)
+      }
+      .mkString(separator)
+
   override def toString: String =
     this.show
 }
diff --git a/core/shared/src/main/scala/org/http4s/MessageFailure.scala b/core/shared/src/main/scala/org/http4s/MessageFailure.scala
index 0197c86803..616ca586e6 100644
--- a/core/shared/src/main/scala/org/http4s/MessageFailure.scala
+++ b/core/shared/src/main/scala/org/http4s/MessageFailure.scala
@@ -132,7 +132,7 @@ sealed abstract class UnsupportedMediaTypeFailure extends DecodeFailure with NoS
 
   protected def sanitizedResponsePrefix: String
   protected def expectedMsg: String =
-    s"Expected one of the following media ranges: ${expected.map(_.show).mkString(", ")}"
+    s"Expected one of the following media ranges: ${expected.iterator.map(_.show).mkString(", ")}"
   protected def responseMsg: String = s"$sanitizedResponsePrefix. $expectedMsg"
 
   def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F] =
diff --git a/core/shared/src/main/scala/org/http4s/RequestCookieJar.scala b/core/shared/src/main/scala/org/http4s/RequestCookieJar.scala
index 120b7e4e64..3fba10c33f 100644
--- a/core/shared/src/main/scala/org/http4s/RequestCookieJar.scala
+++ b/core/shared/src/main/scala/org/http4s/RequestCookieJar.scala
@@ -72,7 +72,7 @@ class RequestCookieJar private (val cookies: List[RequestCookie]) extends AnyVal
     new RequestCookieJar(cookies.filter(c => p(c.name)))
 
   override def toString(): String =
-    s"RequestCookieJar(${cookies.map(_.renderString).mkString("\n")})"
+    s"RequestCookieJar(${cookies.iterator.map(_.renderString).mkString("\n")})"
 
   def ++(cookies: collection.Iterable[RequestCookie]) =
     new RequestCookieJar(this.cookies ++ cookies)
diff --git a/core/shared/src/main/scala/org/http4s/internal/CurlConverter.scala b/core/shared/src/main/scala/org/http4s/internal/CurlConverter.scala
index f742b4dabc..a1ca77cf5e 100644
--- a/core/shared/src/main/scala/org/http4s/internal/CurlConverter.scala
+++ b/core/shared/src/main/scala/org/http4s/internal/CurlConverter.scala
@@ -39,6 +39,7 @@ private[http4s] object CurlConverter {
     val preparedHeaders = headers
       .redactSensitive(redactHeadersWhen)
       .headers
+      .iterator
       .map { header =>
         s"""--header '${escapeQuotationMarks(s"${header.name}: ${header.value}")}'"""
       }
diff --git a/core/shared/src/main/scala/org/http4s/internal/Logger.scala b/core/shared/src/main/scala/org/http4s/internal/Logger.scala
index f5ddd91d1b..5b047dc37d 100644
--- a/core/shared/src/main/scala/org/http4s/internal/Logger.scala
+++ b/core/shared/src/main/scala/org/http4s/internal/Logger.scala
@@ -34,7 +34,7 @@ object Logger {
       redactHeadersWhen: CIString => Boolean = Headers.SensitiveHeaders.contains,
   ): String =
     if (logHeaders)
-      message.headers.redactSensitive(redactHeadersWhen).headers.mkString("Headers(", ", ", ")")
+      message.headers.mkString("Headers(", ", ", ")", redactHeadersWhen)
     else ""
 
   def defaultLogBody[F[_]: Concurrent](
diff --git a/core/shared/src/main/scala/org/http4s/internal/package.scala b/core/shared/src/main/scala/org/http4s/internal/package.scala
index b0f6dc9f90..554c42ec29 100644
--- a/core/shared/src/main/scala/org/http4s/internal/package.scala
+++ b/core/shared/src/main/scala/org/http4s/internal/package.scala
@@ -160,7 +160,7 @@ package object internal extends InternalPlatform {
     var i = 0
     val len = s.length
     while (i < len) {
-      // Strings are equal igoring case if either their uppercase or lowercase
+      // Strings are equal ignoring case if either their uppercase or lowercase
       // forms are equal. Equality of one does not imply the other, so we need
       // to go in both directions. A character is not guaranteed to make this
       // round trip, but it doesn't matter as long as all equal characters
diff --git a/core/shared/src/main/scala/org/http4s/util/Renderable.scala b/core/shared/src/main/scala/org/http4s/util/Renderable.scala
index 52ee4e0750..21df671e81 100644
--- a/core/shared/src/main/scala/org/http4s/util/Renderable.scala
+++ b/core/shared/src/main/scala/org/http4s/util/Renderable.scala
@@ -206,11 +206,15 @@ trait Writer { self =>
       start: String = "",
       end: String = "",
   ): this.type =
-    NonEmptyList.fromList(s) match {
-      case Some(s) => addNel(s, sep, start, end)
-      case None =>
+    s match {
+      case Nil =>
         append(start)
         append(end)
+      case x :: list =>
+        append(start)
+        append(x)
+        list.foreach(s => append(sep).append(s))
+        append(end)
     }
 
   def addNel[T: Renderer](
diff --git a/docs/docs/deployment.md b/docs/docs/deployment.md
index 64bb0defb2..3c186bda8a 100644
--- a/docs/docs/deployment.md
+++ b/docs/docs/deployment.md
@@ -121,5 +121,5 @@ At this point we may want to package the binary in a docker container, integrate
 
 As an alternative to executing via the JVM, GraalVM's native-image allows us to execute as a native binary. For example, in Linux environments this might be known as an `ELF` format. There are a number of ways to generate a native image, be it dynamic or static.
 
-A dynamic image is less portable because it depends on shared libary files on each Linux host. Then similar to creating an assembly JAR containing all our Java dependencies, when we build a static native image, we build
+A dynamic image is less portable because it depends on shared library files on each Linux host. Then similar to creating an assembly JAR containing all our Java dependencies, when we build a static native image, we build
 a more portable assembly including all the dependencies to run our binary across multiple platforms. For example we could expect a static ELF-64 binary to work across multiple linux distributions of different versions for the same architecture.
diff --git a/docs/docs/error-handling.md b/docs/docs/error-handling.md
index d4b4243dd6..26dfe77467 100644
--- a/docs/docs/error-handling.md
+++ b/docs/docs/error-handling.md
@@ -12,9 +12,9 @@ include:
   syntactically correct, but semantically incorrect.  A well-formed
   JSON request that is missing expected fields may generate this
   failure.
-* `MediaTypeMissing`: indiciates that the message had no media type,
+* `MediaTypeMissing`: indicates that the message had no media type,
   and the server wasn't willing to infer it.
-* `MediaTypeMismatch`: indiciates that the server received a media
+* `MediaTypeMismatch`: indicates that the server received a media
   type that it wasn't prepared to handle.
 
 ## Logging
diff --git a/ember-core/shared/src/main/scala/org/http4s/ember/core/Encoder.scala b/ember-core/shared/src/main/scala/org/http4s/ember/core/Encoder.scala
index ee341c7cfb..ca023a2a73 100644
--- a/ember-core/shared/src/main/scala/org/http4s/ember/core/Encoder.scala
+++ b/ember-core/shared/src/main/scala/org/http4s/ember/core/Encoder.scala
@@ -139,7 +139,14 @@ private[ember] object Encoder {
           }
         }
 
-        if (!chunked && !appliedContentLength && !NoPayloadMethods.contains(req.method)) {
+        if (
+          !appliedContentLength && req.entity == Entity.Empty && !NoPayloadMethods.contains(
+            req.method
+          )
+        ) {
+          stringBuilder.append(zeroContentLengthRaw).append(CRLF)
+          chunked = false
+        } else if (!chunked && !appliedContentLength && !NoPayloadMethods.contains(req.method)) {
           stringBuilder.append(chunkedTransferEncodingHeaderRaw).append(CRLF)
           chunked = true
           ()
diff --git a/ember-server/shared/src/main/scala/org/http4s/ember/server/EmberServerBuilder.scala b/ember-server/shared/src/main/scala/org/http4s/ember/server/EmberServerBuilder.scala
index 48abc6cad3..5099e9ec4b 100644
--- a/ember-server/shared/src/main/scala/org/http4s/ember/server/EmberServerBuilder.scala
+++ b/ember-server/shared/src/main/scala/org/http4s/ember/server/EmberServerBuilder.scala
@@ -166,6 +166,11 @@ final class EmberServerBuilder[F[_]: Async: Network] private (
   def withoutUnixSocketConfig: EmberServerBuilder[F] =
     copy(unixSocketConfig = None)
 
+  def withAdditionalSocketOptions(
+      additionalSocketOptions: List[SocketOption]
+  ): EmberServerBuilder[F] =
+    copy(additionalSocketOptions = additionalSocketOptions)
+
   def build: Resource[F, Server] =
     for {
       sg <- sgOpt.getOrElse(Network[F]).pure[Resource[F, *]]
diff --git a/ember-server/shared/src/main/scala/org/http4s/ember/server/internal/Shutdown.scala b/ember-server/shared/src/main/scala/org/http4s/ember/server/internal/Shutdown.scala
index e0660ada63..fe71305826 100644
--- a/ember-server/shared/src/main/scala/org/http4s/ember/server/internal/Shutdown.scala
+++ b/ember-server/shared/src/main/scala/org/http4s/ember/server/internal/Shutdown.scala
@@ -100,7 +100,7 @@ private[server] object Shutdown {
         override val signal: F[Unit] = unblock.get
         override val newConnection: F[Unit] = F.unit
         override val removeConnection: F[Unit] = F.unit
-        override val trackConnection: Stream[F, Unit] = Stream.empty
+        override val trackConnection: Stream[F, Unit] = Stream.emit(())
       }
     }
 
diff --git a/flake.lock b/flake.lock
index 5754659535..0fb12b26b4 100644
--- a/flake.lock
+++ b/flake.lock
@@ -6,11 +6,11 @@
         "nixpkgs": "nixpkgs"
       },
       "locked": {
-        "lastModified": 1663445644,
-        "narHash": "sha256-+xVlcK60x7VY1vRJbNUEAHi17ZuoQxAIH4S4iUFUGBA=",
+        "lastModified": 1666548262,
+        "narHash": "sha256-4DyN4KXqQQsCw0vCXkMThw4b5Q4/q87ZZgRb4st8COc=",
         "owner": "numtide",
         "repo": "devshell",
-        "rev": "e3dc3e21594fe07bdb24bdf1c8657acaa4cb8f66",
+        "rev": "c8ce8ed81726079c398f5f29c4b68a7d6a3c2fa2",
         "type": "github"
       },
       "original": {
@@ -36,11 +36,11 @@
     },
     "flake-utils_2": {
       "locked": {
-        "lastModified": 1659877975,
-        "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
+        "lastModified": 1667077288,
+        "narHash": "sha256-bdC8sFNDpT0HK74u9fUkpbf1MEzVYJ+ka7NXCdgBoaA=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
+        "rev": "6ee9ebb6b1ee695d2cacc4faa053a7b9baa76817",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1665296151,
-        "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
+        "lastModified": 1667055375,
+        "narHash": "sha256-xfSTHYxuKRiqF4dcuAFdti1OUvrC2lHpQqCHWUK5/JA=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
+        "rev": "7f9be6a505a31f88499c5d20d11f98accf5ae6ba",
         "type": "github"
       },
       "original": {
@@ -101,11 +101,11 @@
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1665404222,
-        "narHash": "sha256-HH0aa1RAtJojBH4e1oq5p8IGVR3bjBztKEuWAlawfRw=",
+        "lastModified": 1667273020,
+        "narHash": "sha256-eRx8YaRz/VPH7rzFaLVxvbhskFE3uaVW7jjj+kre/R0=",
         "owner": "typelevel",
         "repo": "typelevel-nix",
-        "rev": "ea0308a79e6dba34d0698b130af9d2166e7a99c9",
+        "rev": "49750de3541d9e50fb3294728ee66ea84b15e047",
         "type": "github"
       },
       "original": {
diff --git a/project/build.properties b/project/build.properties
index 563a014da4..6a9f038894 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1,1 +1,1 @@
-sbt.version=1.7.2
+sbt.version=1.7.3
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 4237ae91c3..9f0f5706d9 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -13,4 +13,4 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")
 addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.1")
 addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.7")
 addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0")
-addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.5")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.6")
diff --git a/server/shared/src/main/scala/org/http4s/server/Router.scala b/server/shared/src/main/scala/org/http4s/server/Router.scala
index effd7e3e53..cd6588529e 100644
--- a/server/shared/src/main/scala/org/http4s/server/Router.scala
+++ b/server/shared/src/main/scala/org/http4s/server/Router.scala
@@ -66,7 +66,7 @@ object Router {
     define(mappings: _*)(HttpRoutes.empty[F])
 
   /** Defines an [[HttpRoutes]] based on list of mappings and
-    * a default Service to be used when none in the list match incomming requests.
+    * a default Service to be used when none in the list match incoming requests.
     *
     * The mappings are processed in descending order (longest first) of prefix length.
     */
diff --git a/server/shared/src/main/scala/org/http4s/server/middleware/BracketRequestResponse.scala b/server/shared/src/main/scala/org/http4s/server/middleware/BracketRequestResponse.scala
index 7b7c316e81..6f103994bc 100644
--- a/server/shared/src/main/scala/org/http4s/server/middleware/BracketRequestResponse.scala
+++ b/server/shared/src/main/scala/org/http4s/server/middleware/BracketRequestResponse.scala
@@ -23,7 +23,7 @@ import cats.implicits._
 import org.http4s._
 import org.http4s.server._
 
-/** Middelwares which allow for bracketing on a Request/Response, including
+/** Middlewares which allow for bracketing on a Request/Response, including
   * the completion of the Response body stream.
   *
   * These are analogous to `cats.effect.Bracket` and `fs2.Stream.bracket`. The
diff --git a/server/shared/src/main/scala/org/http4s/server/middleware/UrlFormLifter.scala b/server/shared/src/main/scala/org/http4s/server/middleware/UrlFormLifter.scala
index c4cf26596f..289646453a 100644
--- a/server/shared/src/main/scala/org/http4s/server/middleware/UrlFormLifter.scala
+++ b/server/shared/src/main/scala/org/http4s/server/middleware/UrlFormLifter.scala
@@ -28,7 +28,7 @@ import cats.~>
   *
   * The params are merged into the existing paras _after_ the existing query params. This
   * means that if the query already contains the pair "foo" -> Some("bar"), parameters on
-  * the body must be acessed through `multiParams`.
+  * the body must be accessed through `multiParams`.
   */
 object UrlFormLifter {
   def apply[F[_]: Sync, G[_]: Concurrent](f: G ~> F)(


Test patch:
diff --git a/client/shared/src/test/scala/org/http4s/client/oauth1/OAuthSuite.scala b/client/shared/src/test/scala/org/http4s/client/oauth1/OAuthSuite.scala
index 1df0bd9891..8131a816c9 100644
--- a/client/shared/src/test/scala/org/http4s/client/oauth1/OAuthSuite.scala
+++ b/client/shared/src/test/scala/org/http4s/client/oauth1/OAuthSuite.scala
@@ -24,7 +24,6 @@ import org.http4s.client.oauth1
 import org.http4s.client.oauth1.ProtocolParameter._
 import org.http4s.client.oauth1.SignatureAlgorithm.Names._
 import org.http4s.headers.Authorization
-import org.typelevel.ci._
 
 class OAuthSuite extends Http4sSuite {
   // some params taken from http://oauth.net/core/1.0/#anchor30, others from
@@ -82,7 +81,7 @@ class OAuthSuite extends Http4sSuite {
       )
       .map { auth =>
         val creds = auth.credentials
-        assertEquals(creds.authScheme, ci"OAuth")
+        assertEquals(creds.authScheme, AuthScheme.OAuth)
       }
   }
 
diff --git a/ember-core/shared/src/test/scala/org/http4s/ember/core/EncoderSuite.scala b/ember-core/shared/src/test/scala/org/http4s/ember/core/EncoderSuite.scala
index 37c6d5e834..c035919324 100644
--- a/ember-core/shared/src/test/scala/org/http4s/ember/core/EncoderSuite.scala
+++ b/ember-core/shared/src/test/scala/org/http4s/ember/core/EncoderSuite.scala
@@ -21,7 +21,6 @@ import cats.effect.Concurrent
 import cats.effect.IO
 import cats.syntax.all._
 import fs2._
-import org.http4s.headers.`Content-Length`
 import org.http4s.syntax.literals._
 
 class EncoderSuite extends Http4sSuite {
@@ -47,7 +46,7 @@ class EncoderSuite extends Http4sSuite {
         .map(stripLines)
   }
 
-  test("reqToBytes should encode a no body request correctly") {
+  test("reqToBytes should encode a no body GET request correctly") {
     val req = Request[IO](Method.GET, uri"http://www.google.com")
     val expected =
       """GET / HTTP/1.1
@@ -114,8 +113,20 @@ class EncoderSuite extends Http4sSuite {
     Helpers.encodeRequestRig(req).assertEquals(expected)
   }
 
+  test("reqToBytes should encode a no body POST request correctly") {
+    val req = Request[IO](Method.POST)
+
+    val expected =
+      """POST / HTTP/1.1
+      |Content-Length: 0
+      |
+      |""".stripMargin
+
+    Helpers.encodeRequestRig(req).assertEquals(expected)
+  }
+
   test("respToBytes should encode a no body response correctly") {
-    val resp = Response[IO](Status.Ok).putHeaders(`Content-Length`.zero)
+    val resp = Response[IO](Status.Ok)
 
     val expected =
       """HTTP/1.1 200 OK
@@ -138,6 +149,20 @@ class EncoderSuite extends Http4sSuite {
     Helpers.encodeResponseRig(resp).assertEquals(expected)
   }
 
+  test("reqToBytes should encode a no body request correctly with stream") {
+    val req = Request[IO](Method.POST, entity = Entity.Default(Stream.chunk(Chunk.empty), None))
+
+    val expected =
+      """POST / HTTP/1.1
+      |Transfer-Encoding: chunked
+      |
+      |0
+      |
+      |""".stripMargin
+
+    Helpers.encodeRequestRig(req).assertEquals(expected)
+  }
+
   test("respToBytes should encode a no body response correctly with stream") {
     val resp = Response[IO](Status.Ok, entity = Entity.Default(Stream.chunk(Chunk.empty), None))
 
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 5b47e39da4..057308a047 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
@@ -28,6 +28,8 @@ import org.http4s.ember.client.EmberClientBuilder
 import org.http4s.implicits._
 import org.http4s.server.Server
 
+import scala.concurrent.duration._
+
 class EmberServerSuite extends Http4sSuite {
 
   def service[F[_]](implicit F: Async[F]): HttpApp[F] = {
@@ -47,12 +49,15 @@ class EmberServerSuite extends Http4sSuite {
   def url(address: SocketAddress[Host], path: String = ""): String =
     s"http://${Uri.Host.fromIp4sHost(address.host).renderString}:${address.port.value}$path"
 
-  val serverResource: Resource[IO, Server] =
-    EmberServerBuilder
-      .default[IO]
-      .withPort(port"0")
-      .withHttpApp(service[IO])
-      .build
+  def serverResource(
+      f: EmberServerBuilder[IO] => EmberServerBuilder[IO] = identity
+  ): Resource[IO, Server] =
+    f(
+      EmberServerBuilder
+        .default[IO]
+        .withPort(port"0")
+        .withHttpApp(service[IO])
+    ).build
 
   private val client = ResourceFunFixture(EmberClientBuilder.default[IO].build)
 
@@ -75,13 +80,19 @@ class EmberServerSuite extends Http4sSuite {
   }
 
   client.test("server shuts down after exiting resource scope") { client =>
-    serverResource.use(server => IO.pure(server.address)).flatMap { address =>
+    serverResource().use(server => IO.pure(server.address)).flatMap { address =>
       client
         .get(url(address))(_.status.pure[IO])
         .intercept[ConnectException]
     }
   }
 
+  client.test("shutdown timeout of 0 doesn't reset connections") { client =>
+    serverResource(_.withShutdownTimeout(0.nanos)).use { server =>
+      client.expect[String](url(server.address)).assertEquals("Hello!")
+    }
+  }
+
   server().test("server startup fails if address is already in use") { server =>
     EmberServerBuilder
       .default[IO]
diff --git a/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthMiddlewareSuite.scala b/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthMiddlewareSuite.scala
index 4a2e03ab1f..c7b989d606 100644
--- a/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthMiddlewareSuite.scala
+++ b/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthMiddlewareSuite.scala
@@ -178,7 +178,7 @@ class AuthMiddlewareSuite extends Http4sSuite {
     service.orNotFound(Request[IO](method = Method.GET)).map(_.status).assertEquals(Unauthorized)
   }
 
-  test("compose authedRoutesand not fall through") {
+  test("compose authedRoutes and not fall through") {
     val userId: User = 42
 
     val authUser: Kleisli[OptionT[IO, *], Request[IO], User] =
diff --git a/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthenticationSuite.scala b/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthenticationSuite.scala
index 4898e084b4..5da91acb67 100644
--- a/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthenticationSuite.scala
+++ b/server/shared/src/test/scala/org/http4s/server/middleware/authentication/AuthenticationSuite.scala
@@ -23,7 +23,6 @@ import org.http4s._
 import org.http4s.dsl.io._
 import org.http4s.headers._
 import org.http4s.syntax.all._
-import org.typelevel.ci._
 
 import scala.concurrent.duration._
 
@@ -204,7 +203,7 @@ class AuthenticationSuite extends Http4sSuite {
             "response" -> response,
             "method" -> method,
           )
-          val header = Authorization(Credentials.AuthParams(ci"Digest", params))
+          val header = Authorization(Credentials.AuthParams(AuthScheme.Digest, params))
 
           val req2 = Request[IO](uri = uri"/", headers = Headers(header))
           digest(req2).flatMap { res2 =>
@@ -351,7 +350,7 @@ class AuthenticationSuite extends Http4sSuite {
           .parTraverse { i =>
             val invalidParams = params.toList.take(i) ++ params.toList.drop(i + 1)
             val header = Authorization(
-              Credentials.AuthParams(ci"Digest", invalidParams.head, invalidParams.tail: _*)
+              Credentials.AuthParams(AuthScheme.Digest, invalidParams.head, invalidParams.tail: _*)
             )
             val req = Request[IO](uri = uri"/", headers = Headers(header))
             digestAuthService.orNotFound(req).map(_.status)
diff --git a/tests/shared/src/test/scala/org/http4s/CharsetRangeSuite.scala b/tests/shared/src/test/scala/org/http4s/CharsetRangeSuite.scala
index 296b442560..c3ae542309 100644
--- a/tests/shared/src/test/scala/org/http4s/CharsetRangeSuite.scala
+++ b/tests/shared/src/test/scala/org/http4s/CharsetRangeSuite.scala
@@ -29,7 +29,7 @@ class CharsetRangeSuite extends Http4sSuite {
     }
   }
 
-  test("atomic charset ranges should match their own charsest") {
+  test("atomic charset ranges should match their own charsets") {
     forAll(arbitrary[CharsetRange.Atom]) { range =>
       range.matches(range.charset)
     }
diff --git a/tests/shared/src/test/scala/org/http4s/HeadersSpec.scala b/tests/shared/src/test/scala/org/http4s/HeadersSpec.scala
index 42d9080aef..d1d94d5dab 100644
--- a/tests/shared/src/test/scala/org/http4s/HeadersSpec.scala
+++ b/tests/shared/src/test/scala/org/http4s/HeadersSpec.scala
@@ -114,6 +114,28 @@ class HeadersSpec extends Http4sSuite {
     assertEquals(Headers.apply(headers), headers)
   }
 
+  test("Headers#mkString") {
+    val h1 = Headers("Header-One" -> "value one", "Header-Two" -> "value two")
+    val h2 = Headers(
+      "Header-One" -> "value one",
+      "Header-Two" -> "value two",
+      "Header-Three" -> "value three",
+    )
+    val expectedString1 = "Headers(Header-One: value one, Header-Two: value two)"
+    val expectedString2 =
+      "Headers(Header-One: value one, Header-Two: value two, Header-Three: <REDACTED>)"
+    val expectedString3 = "Header-One: value one, Header-Two: value two"
+    val expectedString4 = "Header-One: value one, Header-Two: value two, Header-Three: <REDACTED>"
+
+    assertEquals(
+      h1.mkString("Headers(", ", ", ")", Headers.SensitiveHeaders.contains),
+      expectedString1,
+    )
+    assertEquals(h2.mkString("Headers(", ", ", ")", _.toString == "Header-Three"), expectedString2)
+    assertEquals(h1.mkString(", ", Headers.SensitiveHeaders.contains), expectedString3)
+    assertEquals(h2.mkString(", ", _.toString == "Header-Three"), expectedString4)
+  }
+
   checkAll("Monoid[Headers]", MonoidTests[Headers].monoid)
   checkAll("Order[Headers]", OrderTests[Headers].order)
 }
diff --git a/tests/shared/src/test/scala/org/http4s/headers/AcceptPatchSpec.scala b/tests/shared/src/test/scala/org/http4s/headers/AcceptPatchSpec.scala
index abfa4f99c9..9f9d84b384 100644
--- a/tests/shared/src/test/scala/org/http4s/headers/AcceptPatchSpec.scala
+++ b/tests/shared/src/test/scala/org/http4s/headers/AcceptPatchSpec.scala
@@ -28,7 +28,7 @@ class AcceptPatchSpec extends Http4sSuite {
         "Accept-Patch: text/example",
       )
     }
-    test("render should mulitple media types") {
+    test("render should multiple media types") {
       assertEquals(
         `Accept-Patch`(
           NonEmptyList.of(