From c0415fdfadba1b401883b133b58f462cf71e44d7 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 24 Aug 2026 11:59:30 +0100 Subject: [PATCH] fix: use root locale when looking up a response header by name in RouteTest Motivation: `RouteTest#header(name: String)` lowercased the given header name with the JVM default locale before comparing it to `HttpHeader#lowercaseName`, which is built with `toRootLowerCase`. Under a Turkish locale `"If-Match".toLowerCase` yields a dotless i, so the lookup silently returned `None` for any header name containing a capital `I`. The javadsl equivalent (`TestRouteResult`) already uses `toRootLowerCase`. Modification: Use `pekko.util.Helpers.toRootLowerCase` in `RouteTest#header(name)`. Result: Header lookup by name in the Scala testkit is locale independent and matches the javadsl behaviour. Tests: - sbt "http-testkit / Test / testOnly org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTestSpec" - passes with the fix; the new test fails without it - scalafmt --mode diff-ref=origin/main - clean - sbt +mimaReportBinaryIssues - not run; method body change only, no API or binary shape change References: None - found while auditing the code base for locale sensitive lowercasing --- .../http/scaladsl/testkit/RouteTest.scala | 3 ++- .../testkit/ScalatestRouteTestSpec.scala | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/http-testkit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/RouteTest.scala b/http-testkit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/RouteTest.scala index 8fa833417d..38d85309df 100644 --- a/http-testkit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/RouteTest.scala +++ b/http-testkit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/RouteTest.scala @@ -36,6 +36,7 @@ import pekko.stream.{ Materializer, SystemMaterializer } import pekko.stream.scaladsl.Source import pekko.testkit.TestKit import pekko.util.ConstantFun +import pekko.util.Helpers.toRootLowerCase import com.typesafe.config.{ Config, ConfigFactory } @@ -96,7 +97,7 @@ trait RouteTest extends RequestBuilding with WSTestRequestBuilding with RouteTes def charset: HttpCharset = charsetOption.getOrElse(sys.error("Binary entity does not have charset")) def headers: immutable.Seq[HttpHeader] = rawResponse.headers def header[T >: Null <: HttpHeader: ClassTag]: Option[T] = rawResponse.header[T](implicitly[ClassTag[T]]) - def header(name: String): Option[HttpHeader] = rawResponse.headers.find(_.is(name.toLowerCase)) + def header(name: String): Option[HttpHeader] = rawResponse.headers.find(_.is(toRootLowerCase(name))) def status: StatusCode = rawResponse.status def closingExtension: String = chunks.lastOption match { diff --git a/http-testkit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/ScalatestRouteTestSpec.scala b/http-testkit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/ScalatestRouteTestSpec.scala index 18310caa18..c17b49def6 100644 --- a/http-testkit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/ScalatestRouteTestSpec.scala +++ b/http-testkit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/ScalatestRouteTestSpec.scala @@ -13,6 +13,8 @@ package org.apache.pekko.http.scaladsl.testkit +import java.util.Locale + import scala.concurrent.Await import scala.concurrent.Future import scala.concurrent.duration._ @@ -57,6 +59,25 @@ class ScalatestRouteTestSpec extends AnyFreeSpec with Matchers with ScalatestRou } } + "a header lookup by name that is unaffected by the turkish-i problem" in { + val previousLocale = Locale.getDefault + try { + Locale.setDefault(new Locale("tr", "TR")) + // in the turkish locale 'I'.toLowerCase is a dotless i, so a default-locale + // lowercasing of 'If-Match' would not match the header's lowercaseName + val ifMatchHeader = RawHeader("If-Match", "\"xyzzy\"") + Get() ~> { + respondWithHeader(ifMatchHeader) { + complete("abc") + } + } ~> check { + header("If-Match") shouldEqual Some(ifMatchHeader) + } + } finally { + Locale.setDefault(previousLocale) + } + } + "a test using ~!> and some checks" in { // raw here, should have been parsed into modelled header when going through an actual server when using `~!>` val extraHeader = RawHeader("X-Forwarded-Proto", "abc")