This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch 1.4.x
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/1.4.x by this push:
     new df7b180de backport: use Locale.ROOT for toLowerCase/toUpperCase 
(#1086) and RouteTest header lookup (#1223) (#1224)
df7b180de is described below

commit df7b180dedf855197479cfde2b64e0bf52a00e41
Author: PJ Fanning <[email protected]>
AuthorDate: Wed Aug 26 13:00:35 2026 +0100

    backport: use Locale.ROOT for toLowerCase/toUpperCase (#1086) and RouteTest 
header lookup (#1223) (#1224)
    
    * use Locale.ROOT for toLowerCase/toUpperCase (#1086)
    
    (cherry picked from commit 7def2274d7a0fb0f778ed80b791b8b4d51f834e6)
    
    * test: add turkish-i regression tests for Locale.ROOT lowercasing
    
    Motivation:
    The backported Locale.ROOT change has no test coverage beyond the existing
    HttpCharsets case, so a regression would go unnoticed.
    
    Modification:
    Extend TurkishISpec with cases that run under a tr-TR default locale and 
cover
    `HttpHeader.parse`, `Uri` scheme normalization, `MediaTypes.forExtension` 
and
    `ErrorInfo.withErrorHeaderName`.
    
    Result:
    The header-name, scheme, file-extension and error-header-name paths are 
pinned
    against locale sensitive lowercasing.
    
    Tests:
    - sbt "http-core / Test / testOnly 
org.apache.pekko.http.scaladsl.model.TurkishISpec" - 5 passed; all 4 new cases 
fail against 1.4.x without the backported commit
    - scalafmt --mode diff-ref=upstream/1.4.x - clean
    
    References:
    Refs #1086
    
    * ci: bump cache actions to the versions used on main
    
    Motivation:
    The 1.4.x workflows pin coursier/cache-action v8.1.0 (and v6 in two of the 
docs
    workflows) and actions/cache v5.0.4. Workflow runs on this branch end in
    startup_failure before any job is created, while main, which pins newer
    versions of both actions, runs fine.
    
    Modification:
    Pin coursier/cache-action to 95e5b1029b6b86e7bac033ee44a0697d8a527d2d 
(v8.1.1)
    and actions/cache to 55cc8345863c7cc4c66a329aec7e433d2d1c52a9 (v6.1.0) in 
every
    workflow, matching main.
    
    Result:
    CI on 1.4.x branches starts and runs as it does on main.
    
    Tests:
    - Not run - CI configuration only; verified by the workflow runs on this PR
    
    References:
    Refs #1224
    
    * fix: use root locale when looking up a response header by name in 
RouteTest (#1223)
    
    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
    
    (cherry picked from commit 08e174c4ce2a1bf6c8371bf75e572093840bbd54)
    
    * ci: bump sbt/setup-sbt to v1.5.7
    
    Motivation:
    Jobs on this branch fail during "Set up job" with
    
      The action carabiner-dev/actions/install/ampel@2a11d59 is not allowed in
      apache/pekko-http because all actions must be from a repository owned by
      your enterprise, created by GitHub, or match one of the patterns: ...
    
    sbt/setup-sbt v1.5.0 pulls that action in transitively and it is not on the 
ASF
    allowlist. main pins v1.5.7 and its jobs run.
    
    Modification:
    Pin sbt/setup-sbt to 8feba82adc7f01ddcf8165b86f778bdb5b82cebc (v1.5.7) in 
every
    workflow, matching main. Two docs workflows were still on the floating v1 
tag
    and on v1.1.9.
    
    Result:
    Jobs get past action setup on 1.4.x branches.
    
    Tests:
    - Not run - CI configuration only; verified by the workflow runs on this PR
    
    References:
    Refs #1224
---
 .github/workflows/dependency-graph.yml             |  2 +-
 .github/workflows/headers.yml                      |  4 +--
 .github/workflows/link-validator.yml               |  4 +--
 .github/workflows/nightly.yml                      |  6 ++---
 .github/workflows/publish-1.0-docs.yml             |  4 +--
 .github/workflows/publish-1.0-snapshots.yml        |  4 +--
 .github/workflows/publish-1.1-docs.yml             |  4 +--
 .github/workflows/publish-1.1-snapshots.yml        |  4 +--
 .github/workflows/publish-1.2-docs.yml             |  4 +--
 .github/workflows/publish-1.3-docs.yml             |  4 +--
 .github/workflows/publish.yml                      |  8 +++---
 .github/workflows/stage-release-candidate.yml      |  2 +-
 .github/workflows/validate-and-test.yml            | 12 ++++-----
 .../http/impl/engine/http2/Http2Blueprint.scala    |  6 ++---
 .../http/impl/settings/ParserSettingsImpl.scala    |  3 ++-
 .../apache/pekko/http/javadsl/ConnectHttp.scala    |  8 +++---
 .../http/javadsl/settings/ParserSettings.scala     |  3 ++-
 .../pekko/http/scaladsl/model/ErrorInfo.scala      |  6 +++--
 .../pekko/http/scaladsl/model/HttpHeader.scala     |  3 ++-
 .../pekko/http/scaladsl/model/MediaType.scala      |  9 ++++---
 .../org/apache/pekko/http/scaladsl/model/Uri.scala |  3 ++-
 .../http/scaladsl/settings/ParserSettings.scala    |  3 ++-
 .../pekko/http/scaladsl/model/TurkishISpec.scala   | 29 ++++++++++++++++++++++
 .../pekko/http/scaladsl/testkit/RouteTest.scala    |  3 ++-
 .../scaladsl/testkit/ScalatestRouteTestSpec.scala  | 21 ++++++++++++++++
 .../server/directives/HeaderDirectives.scala       |  5 ++--
 .../server/directives/MethodDirectives.scala       |  4 ++-
 .../PredefinedFromStringUnmarshallers.scala        |  3 ++-
 28 files changed, 117 insertions(+), 54 deletions(-)

diff --git a/.github/workflows/dependency-graph.yml 
b/.github/workflows/dependency-graph.yml
index 46d7f5c40..eb8713101 100644
--- a/.github/workflows/dependency-graph.yml
+++ b/.github/workflows/dependency-graph.yml
@@ -28,7 +28,7 @@ jobs:
     steps:
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
       - uses: 
scalacenter/sbt-dependency-submission@f43202114d7522a4b233e052f82c2eea8d658134 
# v3.2.1
         with:
           modules-ignore: pekko-http-tests_3 pekko-http-docs_3
diff --git a/.github/workflows/headers.yml b/.github/workflows/headers.yml
index e152f1f11..127f62627 100644
--- a/.github/workflows/headers.yml
+++ b/.github/workflows/headers.yml
@@ -37,10 +37,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Enable jvm-opts
         run: cp .jvmopts-ci .jvmopts
diff --git a/.github/workflows/link-validator.yml 
b/.github/workflows/link-validator.yml
index 6a377689d..ae383c064 100644
--- a/.github/workflows/link-validator.yml
+++ b/.github/workflows/link-validator.yml
@@ -38,10 +38,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Setup Coursier
         uses: coursier/setup-action@fd1707a76b027efdfb66ca79318b4d29b72e5a02 # 
v3.0.0
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index d5e49b123..765138976 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -38,13 +38,13 @@ jobs:
           java-version: ${{ matrix.JDK }}
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Cache Build Target
-        uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
+        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
         with:
           path: project/**/target
           key: build-target-${{ hashFiles('**/*.sbt', 
'project/build.properties', 'project/**/*.scala') }}
diff --git a/.github/workflows/publish-1.0-docs.yml 
b/.github/workflows/publish-1.0-docs.yml
index f6f04988a..1b70f9406 100644
--- a/.github/workflows/publish-1.0-docs.yml
+++ b/.github/workflows/publish-1.0-docs.yml
@@ -40,10 +40,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Build Documentation
         run: |-
diff --git a/.github/workflows/publish-1.0-snapshots.yml 
b/.github/workflows/publish-1.0-snapshots.yml
index 5d96cd06a..ecc2b95e0 100644
--- a/.github/workflows/publish-1.0-snapshots.yml
+++ b/.github/workflows/publish-1.0-snapshots.yml
@@ -33,10 +33,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Install graphviz
         run: sudo apt-get install -y graphviz
diff --git a/.github/workflows/publish-1.1-docs.yml 
b/.github/workflows/publish-1.1-docs.yml
index 2f8c0f76a..751585fff 100644
--- a/.github/workflows/publish-1.1-docs.yml
+++ b/.github/workflows/publish-1.1-docs.yml
@@ -40,10 +40,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Build Documentation
         run: |-
diff --git a/.github/workflows/publish-1.1-snapshots.yml 
b/.github/workflows/publish-1.1-snapshots.yml
index b5e8e689e..f7688938c 100644
--- a/.github/workflows/publish-1.1-snapshots.yml
+++ b/.github/workflows/publish-1.1-snapshots.yml
@@ -33,10 +33,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Install graphviz
         run: sudo apt-get install -y graphviz
diff --git a/.github/workflows/publish-1.2-docs.yml 
b/.github/workflows/publish-1.2-docs.yml
index 882394b27..7c73ec300 100644
--- a/.github/workflows/publish-1.2-docs.yml
+++ b/.github/workflows/publish-1.2-docs.yml
@@ -39,10 +39,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@v1
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@v6
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Build Documentation
         run: |-
diff --git a/.github/workflows/publish-1.3-docs.yml 
b/.github/workflows/publish-1.3-docs.yml
index d3e36c949..85de49687 100644
--- a/.github/workflows/publish-1.3-docs.yml
+++ b/.github/workflows/publish-1.3-docs.yml
@@ -40,10 +40,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@6c68d2fe8dfbc0a0534d70101baa2e0420e1a506 # v1.1.9
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@v6
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Build Documentation
         run: |-
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8332f8332..e303ecaa1 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -41,10 +41,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Install graphviz
         run: sudo apt-get install -y graphviz
@@ -74,10 +74,10 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Build Documentation
         run: |-
diff --git a/.github/workflows/stage-release-candidate.yml 
b/.github/workflows/stage-release-candidate.yml
index d0abc8fdf..613cf1bf4 100644
--- a/.github/workflows/stage-release-candidate.yml
+++ b/.github/workflows/stage-release-candidate.yml
@@ -212,7 +212,7 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Install Graphviz
         run: |-
diff --git a/.github/workflows/validate-and-test.yml 
b/.github/workflows/validate-and-test.yml
index fe48f6304..e440184c6 100644
--- a/.github/workflows/validate-and-test.yml
+++ b/.github/workflows/validate-and-test.yml
@@ -38,13 +38,13 @@ jobs:
           java-version: 8
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Cache Build Target
-        uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
+        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
         with:
           path: project/**/target
           key: build-target-${{ hashFiles('**/*.sbt', 
'project/build.properties', 'project/**/*.scala') }}
@@ -86,13 +86,13 @@ jobs:
           java-version: ${{ matrix.JDK }}
 
       - name: Install sbt
-        uses: sbt/setup-sbt@66fb4376e81982c7d92a4074170846fff88e2e30 # v1.5.0
+        uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
 
       - name: Cache Coursier cache
-        uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # 
v8.1.0
+        uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d # 
v8.1.1
 
       - name: Cache Build Target
-        uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
+        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
         with:
           path: project/**/target
           key: build-target-${{ hashFiles('**/*.sbt', 
'project/build.properties', 'project/**/*.scala') }}
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
index 3b14c0b86..f732085b0 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
@@ -38,8 +38,8 @@ import pekko.http.scaladsl.settings.{
 import pekko.stream.{ BidiShape, Graph, StreamTcpException, ThrottleMode }
 import pekko.stream.TLSProtocol._
 import pekko.stream.scaladsl.{ BidiFlow, Flow, Keep, Source }
-import pekko.util.ByteString
-import pekko.util.OptionVal
+import pekko.util.{ ByteString, OptionVal }
+import pekko.util.Helpers.toRootLowerCase
 
 import scala.concurrent.duration.{ Duration, FiniteDuration }
 import scala.concurrent.{ ExecutionContext, Future }
@@ -226,7 +226,7 @@ private[http] object Http2Blueprint {
   }
 
   private[http2] def frameTypeAliasToFrameTypeName(frameType: String): 
Option[String] = {
-    frameType.toLowerCase match {
+    toRootLowerCase(frameType) match {
       case "reset"         => Some("RstStreamFrame")
       case "headers"       => Some("HeadersFrame")
       case "continuation"  => Some("ContinuationFrame")
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ParserSettingsImpl.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ParserSettingsImpl.scala
index c8999d0ce..3a47781d1 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ParserSettingsImpl.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/ParserSettingsImpl.scala
@@ -23,6 +23,7 @@ import pekko.http.scaladsl.settings.ParserSettings.{
   IllegalResponseHeaderValueProcessingMode
 }
 import pekko.util.ConstantFun
+import pekko.util.Helpers.toRootLowerCase
 import com.typesafe.config.Config
 
 import scala.collection.JavaConverters._
@@ -115,7 +116,7 @@ object ParserSettingsImpl extends 
SettingsCompanionImpl[ParserSettingsImpl]("pek
       Uri.ParsingMode(c.getString("uri-parsing-mode")),
       CookieParsingMode(c.getString("cookie-parsing-mode")),
       c.getBoolean("illegal-header-warnings"),
-      
c.getStringList("ignore-illegal-header-for").asScala.map(_.toLowerCase).toSet,
+      
c.getStringList("ignore-illegal-header-for").asScala.map(toRootLowerCase).toSet,
       ErrorLoggingVerbosity(c.getString("error-logging-verbosity")),
       
IllegalResponseHeaderNameProcessingMode(c.getString("illegal-response-header-name-processing-mode")),
       
IllegalResponseHeaderValueProcessingMode(c.getString("illegal-response-header-value-processing-mode")),
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/ConnectHttp.scala 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/ConnectHttp.scala
index 7191d0182..27d3bc999 100644
--- a/http-core/src/main/scala/org/apache/pekko/http/javadsl/ConnectHttp.scala
+++ b/http-core/src/main/scala/org/apache/pekko/http/javadsl/ConnectHttp.scala
@@ -13,12 +13,12 @@
 
 package org.apache.pekko.http.javadsl
 
-import java.util.Locale
 import java.util.Optional
 
 import org.apache.pekko
 import pekko.annotation.{ DoNotInherit, InternalApi }
 import pekko.http.javadsl.model.Uri
+import pekko.util.Helpers.toRootLowerCase
 import pekko.util.OptionConverters._
 
 @DoNotInherit
@@ -71,7 +71,7 @@ object ConnectHttp {
   }
 
   private def toHost(uriHost: Uri, port: Int): ConnectHttp = {
-    val s = uriHost.scheme.toLowerCase(Locale.ROOT)
+    val s = toRootLowerCase(uriHost.scheme)
     if (s == "https") new ConnectHttpsImpl(uriHost.host.address, 
effectivePort(s, port), context = Optional.empty())
     else new ConnectHttpImpl(uriHost.host.address, effectivePort(s, port))
   }
@@ -114,7 +114,7 @@ object ConnectHttp {
   }
 
   private def toHostHttps(uriHost: Uri, port: Int): ConnectWithHttps = {
-    val s = uriHost.scheme.toLowerCase(Locale.ROOT)
+    val s = toRootLowerCase(uriHost.scheme)
     require(s == "" || s == "https", "toHostHttps used with non https scheme! 
Was: " + uriHost)
     new ConnectHttpsImpl(uriHost.host.address, effectivePort("https", port), 
context = Optional.empty())
   }
@@ -125,7 +125,7 @@ object ConnectHttp {
   }
 
   private def effectivePort(scheme: String, port: Int): Int = {
-    val s = scheme.toLowerCase(Locale.ROOT)
+    val s = toRootLowerCase(scheme)
     if (port >= 0) port
     else if (s == "https" || s == "wss") 443
     else if (s == "http" || s == "ws") 80
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/ParserSettings.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/ParserSettings.scala
index 6eaa2aded..2b0b3240c 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/ParserSettings.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/ParserSettings.scala
@@ -23,6 +23,7 @@ import java.{ util => ju }
 
 import pekko.annotation.DoNotInherit
 import pekko.http.impl.util.JavaMapping.Implicits._
+import pekko.util.Helpers.toRootLowerCase
 
 import scala.annotation.varargs
 import scala.collection.JavaConverters._
@@ -90,7 +91,7 @@ abstract class ParserSettings private[pekko] () extends 
BodyPartParser.Settings
     self.copy(includeSslSessionAttribute = newValue)
   def withModeledHeaderParsing(newValue: Boolean): ParserSettings = 
self.copy(modeledHeaderParsing = newValue)
   def withIgnoreIllegalHeaderFor(newValue: List[String]): ParserSettings =
-    self.copy(ignoreIllegalHeaderFor = newValue.map(_.toLowerCase).toSet)
+    self.copy(ignoreIllegalHeaderFor = newValue.map(toRootLowerCase).toSet)
 
   // special ---
 
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ErrorInfo.scala 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ErrorInfo.scala
index 5d73947b5..2b552fe42 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ErrorInfo.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ErrorInfo.scala
@@ -14,7 +14,9 @@
 package org.apache.pekko.http.scaladsl.model
 
 import StatusCodes.ClientError
-import org.apache.pekko.annotation.InternalApi
+import org.apache.pekko
+import pekko.annotation.InternalApi
+import pekko.util.Helpers.toRootLowerCase
 
 /**
  * Two-level model of error information.
@@ -28,7 +30,7 @@ final class ErrorInfo(
     val errorHeaderName: String = "") extends scala.Product with scala.Equals 
with java.io.Serializable {
   def withSummary(newSummary: String) = copy(summary = newSummary)
   def withSummaryPrepended(prefix: String) = withSummary(if (summary.isEmpty) 
prefix else prefix + ": " + summary)
-  def withErrorHeaderName(headerName: String) = new ErrorInfo(summary, detail, 
headerName.toLowerCase)
+  def withErrorHeaderName(headerName: String) = new ErrorInfo(summary, detail, 
toRootLowerCase(headerName))
   def withFallbackSummary(fallbackSummary: String) = if (summary.isEmpty) 
withSummary(fallbackSummary) else this
   def formatPretty = if (summary.isEmpty) detail else if (detail.isEmpty) 
summary else summary + ": " + detail
   def format(withDetail: Boolean): String = if (withDetail) formatPretty else 
summary
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpHeader.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpHeader.scala
index 0434b557d..6a2a47a02 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpHeader.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpHeader.scala
@@ -23,6 +23,7 @@ import pekko.http.impl.model.parser.{ CharacterClasses, 
HeaderParser }
 import pekko.http.javadsl.{ model => jm }
 import pekko.http.scaladsl.model.headers._
 import pekko.util.OptionVal
+import pekko.util.Helpers.toRootLowerCase
 
 import scala.collection.immutable
 
@@ -91,7 +92,7 @@ object HttpHeader {
       val parser = new HeaderParser(value, settings)
       parser.`header-field-value`.run() match {
         case Success(preProcessedValue) =>
-          HeaderParser.parseFull(name.toLowerCase, preProcessedValue, 
settings) match {
+          HeaderParser.parseFull(toRootLowerCase(name), preProcessedValue, 
settings) match {
             case HeaderParser.Success(header) => ParsingResult.Ok(header, Nil)
             case HeaderParser.Failure(info) =>
               val errors = info.withSummaryPrepended(s"Illegal HTTP header 
'$name'") :: Nil
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaType.scala 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaType.scala
index 61643de19..7eef53b92 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaType.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaType.scala
@@ -18,6 +18,7 @@ import pekko.annotation.DoNotInherit
 import pekko.http.impl.util._
 import pekko.http.javadsl.{ model => jm }
 import pekko.http.impl.util.JavaMapping.Implicits._
+import pekko.util.Helpers.toRootLowerCase
 
 /**
  * A MediaType describes the type of the content of an HTTP message entity.
@@ -74,7 +75,7 @@ sealed abstract class MediaType(_mainType: String, _subType: 
String) extends jm.
       case _            => false
     }
 
-  override def hashCode(): Int = value.toLowerCase.hashCode
+  override def hashCode(): Int = toRootLowerCase(value).hashCode
 
   /**
    * JAVA API
@@ -327,12 +328,12 @@ object MediaTypes extends ObjectRegistry[(String, 
String), MediaType] {
 
   private[this] var extensionMap = Map.empty[String, MediaType]
 
-  def forExtensionOption(ext: String): Option[MediaType] = 
extensionMap.get(ext.toLowerCase)
-  def forExtension(ext: String): MediaType = 
extensionMap.getOrElse(ext.toLowerCase, `application/octet-stream`)
+  def forExtensionOption(ext: String): Option[MediaType] = 
extensionMap.get(toRootLowerCase(ext))
+  def forExtension(ext: String): MediaType = 
extensionMap.getOrElse(toRootLowerCase(ext), `application/octet-stream`)
 
   private def registerFileExtensions[T <: MediaType](mediaType: T): T = {
     mediaType.fileExtensions.foreach { ext =>
-      val lcExt = ext.toLowerCase
+      val lcExt = toRootLowerCase(ext)
       require(!extensionMap.contains(lcExt),
         s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and 
'$mediaType'")
       extensionMap = extensionMap.updated(lcExt, mediaType)
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/Uri.scala 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/Uri.scala
index 72e4e7f9d..135ac4cf9 100644
--- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/Uri.scala
+++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/Uri.scala
@@ -28,6 +28,7 @@ import pekko.http.javadsl.{ model => jm }
 import pekko.http.impl.model.parser.UriParser
 import pekko.http.impl.model.parser.CharacterClasses._
 import pekko.http.impl.util._
+import pekko.util.Helpers.toRootLowerCase
 import Uri._
 
 /**
@@ -868,7 +869,7 @@ object Uri {
       } else if (allLower) -1
       else -2
     verify() match {
-      case -2 => scheme.toLowerCase
+      case -2 => toRootLowerCase(scheme)
       case -1 => scheme
       case ix => fail(s"Invalid URI scheme, unexpected character at pos $ix 
('${scheme.charAt(ix)}')")
     }
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/ParserSettings.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/ParserSettings.scala
index 82b919b98..980d44c9a 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/ParserSettings.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/ParserSettings.scala
@@ -25,6 +25,7 @@ import pekko.http.impl.util._
 import pekko.http.javadsl.model
 import pekko.http.scaladsl.model._
 import pekko.http.scaladsl.{ settings => js }
+import pekko.util.Helpers.toRootLowerCase
 import pekko.util.OptionConverters._
 import com.typesafe.config.Config
 
@@ -123,7 +124,7 @@ abstract class ParserSettings private[pekko] () extends 
pekko.http.javadsl.setti
     self.copy(includeSslSessionAttribute = newValue)
   override def withModeledHeaderParsing(newValue: Boolean): ParserSettings = 
self.copy(modeledHeaderParsing = newValue)
   override def withIgnoreIllegalHeaderFor(newValue: List[String]): 
ParserSettings =
-    self.copy(ignoreIllegalHeaderFor = newValue.map(_.toLowerCase).toSet)
+    self.copy(ignoreIllegalHeaderFor = newValue.map(toRootLowerCase).toSet)
 
   // overloads for idiomatic Scala use
   def withUriParsingMode(newValue: Uri.ParsingMode): ParserSettings = 
self.copy(uriParsingMode = newValue)
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/TurkishISpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/TurkishISpec.scala
index 58d6a89f6..76f9fdede 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/TurkishISpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/TurkishISpec.scala
@@ -40,5 +40,34 @@ class TurkishISpec extends AnyWordSpec with Matchers {
         Locale.setDefault(previousLocale)
       }
     }
+
+    "parse a header name containing a capital I in the turkish locale" in 
withTurkishLocale {
+      HttpHeader.parse("If-Match", "\"xyzzy\"") match {
+        case HttpHeader.ParsingResult.Ok(header, Nil) => header shouldBe 
a[headers.`If-Match`]
+        case other                                    => fail(s"Expected a 
modelled If-Match header but got $other")
+      }
+    }
+
+    "normalize a uri scheme containing a capital I in the turkish locale" in 
withTurkishLocale {
+      Uri(scheme = "IPP", authority = 
Uri.Authority(Uri.Host("example.com"))).scheme shouldEqual "ipp"
+    }
+
+    "resolve a media type for an upper case file extension in the turkish 
locale" in withTurkishLocale {
+      MediaTypes.forExtension("TIFF") shouldEqual MediaTypes.`image/tiff`
+    }
+
+    "lowercase an error header name in the turkish locale" in 
withTurkishLocale {
+      ErrorInfo("summary", 
"detail").withErrorHeaderName("If-Match").errorHeaderName shouldEqual "if-match"
+    }
+  }
+
+  private def withTurkishLocale(body: => Any): Unit = {
+    val previousLocale = Locale.getDefault
+    try {
+      Locale.setDefault(new Locale("tr", "TR"))
+      body
+    } finally {
+      Locale.setDefault(previousLocale)
+    }
   }
 }
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 c33ca493a..bf961219f 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
@@ -30,6 +30,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 }
 
 import scala.collection.immutable
@@ -95,7 +96,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 63eb780ed..4ef079b49 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.duration._
 import org.apache.pekko
 import pekko.testkit._
@@ -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")
diff --git 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/HeaderDirectives.scala
 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/HeaderDirectives.scala
index 54c4d9fa1..29d94b920 100644
--- 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/HeaderDirectives.scala
+++ 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/HeaderDirectives.scala
@@ -18,6 +18,7 @@ import org.apache.pekko
 import pekko.http.impl.util._
 import pekko.http.scaladsl.model._
 import pekko.http.scaladsl.model.headers._
+import pekko.util.Helpers.toRootLowerCase
 
 import scala.reflect.ClassTag
 import scala.util.control.NonFatal
@@ -92,7 +93,7 @@ trait HeaderDirectives {
    * @group header
    */
   def headerValueByName(headerName: String): Directive1[String] =
-    headerValue(optionalValue(headerName.toLowerCase)) | 
reject(MissingHeaderRejection(headerName))
+    headerValue(optionalValue(toRootLowerCase(headerName))) | 
reject(MissingHeaderRejection(headerName))
 
   /**
    * Extracts the first HTTP request header of the given type.
@@ -147,7 +148,7 @@ trait HeaderDirectives {
    * @group header
    */
   def optionalHeaderValueByName(headerName: String): 
Directive1[Option[String]] = {
-    val lowerCaseName = headerName.toRootLowerCase
+    val lowerCaseName = toRootLowerCase(headerName)
     extract(_.request.headers.collectFirst {
       case h: HttpHeader if h.is(lowerCaseName) => h.value
     })
diff --git 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala
 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala
index 7edf7c6e0..3d609d957 100644
--- 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala
+++ 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala
@@ -14,6 +14,8 @@
 package org.apache.pekko.http.scaladsl.server
 package directives
 
+import java.util.Locale
+
 import org.apache.pekko
 import pekko.http.scaladsl.model.{ HttpMethod, StatusCodes }
 import pekko.http.scaladsl.model.HttpMethods._
@@ -111,7 +113,7 @@ trait MethodDirectives {
   def overrideMethodWithParameter(paramName: String): Directive0 =
     parameter(paramName.optional).flatMap {
       case Some(method) =>
-        getForKey(method.toUpperCase) match {
+        getForKey(method.toUpperCase(Locale.ROOT)) match {
           case Some(m) => mapRequest(_.withMethod(m))
           case _       => complete(StatusCodes.NotImplemented)
         }
diff --git 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/unmarshalling/PredefinedFromStringUnmarshallers.scala
 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/unmarshalling/PredefinedFromStringUnmarshallers.scala
index a865fcedb..e18eab4bb 100755
--- 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/unmarshalling/PredefinedFromStringUnmarshallers.scala
+++ 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/unmarshalling/PredefinedFromStringUnmarshallers.scala
@@ -19,6 +19,7 @@ import scala.collection.immutable
 import org.apache.pekko
 import pekko.http.scaladsl.util.FastFuture
 import pekko.util.ByteString
+import pekko.util.Helpers.toRootLowerCase
 
 trait PredefinedFromStringUnmarshallers {
 
@@ -48,7 +49,7 @@ trait PredefinedFromStringUnmarshallers {
 
   implicit val booleanFromStringUnmarshaller: Unmarshaller[String, Boolean] =
     Unmarshaller.strict[String, Boolean] { string =>
-      string.toLowerCase match {
+      toRootLowerCase(string) match {
         case "true" | "yes" | "on" | "1"  => true
         case "false" | "no" | "off" | "0" => false
         case ""                           => throw 
Unmarshaller.NoContentException


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to