From 0de6d0bade712df066c8577df04a050acb30d348 Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Wed, 28 Jan 2026 22:03:56 +1300 Subject: [PATCH 1/2] Support appending header ranges to CorsSettings. Provide a CorsSettings builder method to allow additional header ranges to be added, and the underlying ability to concatenate HttpHeaderRange instances to support it. This supports scenarios where complex CorsSettings defaults are provided by downstream libraries (e.g. pekko-grpc) but still need to be extended/adapted by end user applications. --- .../cors/javadsl/model/HttpHeaderRange.java | 2 + .../new-methods.excludes | 20 ++++ .../pekko/http/cors/CorsJavaMapping.scala | 3 + .../cors/scaladsl/model/HttpHeaderRange.scala | 17 ++- .../scaladsl/model/HttpHeaderRangeSpec.scala | 107 ++++++++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes create mode 100644 http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala diff --git a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java index a293b62bd..4a3a0941f 100644 --- a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java +++ b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java @@ -24,6 +24,8 @@ public abstract class HttpHeaderRange { public abstract boolean matches(String header); + public abstract HttpHeaderRange concat(HttpHeaderRange range); + public static HttpHeaderRange create(String... headers) { return HttpHeaderRange$.MODULE$.apply(Util.convertArray(headers)); } diff --git a/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes b/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes new file mode 100644 index 000000000..5a5786bc2 --- /dev/null +++ b/http-cors/src/main/mima-filters/2.0.x.backwards.excludes/new-methods.excludes @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# New methods added in 2.0.x +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.cors.javadsl.model.HttpHeaderRange.concat") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.cors.scaladsl.model.HttpHeaderRange.concat") diff --git a/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala b/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala index e9ef33e45..e451ff6da 100644 --- a/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala +++ b/http-cors/src/main/scala/org/apache/pekko/http/cors/CorsJavaMapping.scala @@ -27,5 +27,8 @@ private[pekko] object CorsJavaMapping { object Implicits { implicit object CorsSettingsMapping extends JavaMapping.Inherited[javadsl.settings.CorsSettings, scaladsl.settings.CorsSettings] + + implicit object HttpHeaderRangeMapping + extends JavaMapping.Inherited[javadsl.model.HttpHeaderRange, scaladsl.model.HttpHeaderRange] } } diff --git a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala index 454a14cc7..6974ba684 100644 --- a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala +++ b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala @@ -20,19 +20,34 @@ package org.apache.pekko.http.cors.scaladsl.model import scala.collection.immutable.Seq import org.apache.pekko +import pekko.http.cors.CorsJavaMapping.Implicits._ import pekko.http.cors.javadsl +import pekko.http.impl.util.JavaMapping import pekko.util.Helpers -abstract class HttpHeaderRange extends javadsl.model.HttpHeaderRange +sealed abstract class HttpHeaderRange extends javadsl.model.HttpHeaderRange { + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange + + def ++(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = concat(range) +} object HttpHeaderRange { case object `*` extends HttpHeaderRange { def matches(header: String) = true + + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = this } final case class Default(headers: Seq[String]) extends HttpHeaderRange { private val lowercaseHeaders: Seq[String] = headers.map(Helpers.toRootLowerCase) def matches(header: String): Boolean = lowercaseHeaders.contains(Helpers.toRootLowerCase(header)) + + override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = { + JavaMapping.toScala(range) match { + case `*` => `*` + case Default(headers) => Default(this.headers ++ headers) + } + } } def apply(headers: String*): Default = Default(Seq(headers: _*)) diff --git a/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala b/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala new file mode 100644 index 000000000..55d68ac04 --- /dev/null +++ b/http-cors/src/test/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRangeSpec.scala @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pekko.http.cors.scaladsl.model + +import org.apache.pekko.http.scaladsl.model.headers.{ `Content-Type`, Accept } +import org.scalatest.Inspectors +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class HttpHeaderRangeSpec extends AnyWordSpec with Matchers with Inspectors { + + "The `*` range" should { + "match any Header" in { + val headers = Seq( + `Content-Type`.name, + "conTent-tyPe", + Accept.name, + "x-any-random-header-name") + + forAll(headers) { o => HttpHeaderRange.*.matches(o) shouldBe true } + } + + "be printed as `*`" in { + HttpHeaderRange.*.toString shouldBe "*" + } + } + + "The default range" should { + val range = HttpHeaderRange(`Content-Type`.name, Accept.name) + + "match headers ignoring case" in { + val headers = Seq( + `Content-Type`.name, + `Content-Type`.name.toUpperCase(), + `Content-Type`.name.toLowerCase(), + "conTent-tyPe", + Accept.name, + Accept.name.toUpperCase(), + Accept.name.toLowerCase(), + "aCcepT") + + forAll(headers) { o => range.matches(o) shouldBe true } + } + + "not match other headers" in { + val headers = Seq( + "Content-Type2", + "Content-Typ", + "x-any-random-header-name") + + forAll(headers) { o => range.matches(o) shouldBe false } + } + } + + "Concatenation of ranges" should { + "match both ranges" in { + val range1 = HttpHeaderRange(`Content-Type`.name) + val range2 = HttpHeaderRange(Accept.name) + + val combined = range1 ++ range2 + + val headers = Seq( + `Content-Type`.name, + Accept.name) + val notHeaders = Seq( + "Content-Type2", + "Content-Typ", + "x-any-random-header-name") + + forAll(headers) { o => combined.matches(o) shouldBe true } + forAll(notHeaders) { o => combined.matches(o) shouldBe false } + } + + "combine with the `*` range" in { + val range1 = HttpHeaderRange(`Content-Type`.name) + val starRange = HttpHeaderRange.* + + val combinedBefore = starRange ++ range1 + val combinedAfter = range1 ++ starRange + + val headers = Seq( + `Content-Type`.name, + "conTent-tyPe", + Accept.name, + "x-any-random-header-name") + + forAll(headers) { o => combinedBefore.matches(o) shouldBe true } + forAll(headers) { o => combinedAfter.matches(o) shouldBe true } + } + } + +} From 401aa8434771319675356361c26c851c826c8155 Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Fri, 30 Jan 2026 10:17:34 +1300 Subject: [PATCH 2/2] Add Javadoc and @since 2.0.0 to new methods. --- .../pekko/http/cors/javadsl/model/HttpHeaderRange.java | 5 +++++ .../pekko/http/cors/scaladsl/model/HttpHeaderRange.scala | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java index 4a3a0941f..14e8ef568 100644 --- a/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java +++ b/http-cors/src/main/java/org/apache/pekko/http/cors/javadsl/model/HttpHeaderRange.java @@ -24,6 +24,11 @@ public abstract class HttpHeaderRange { public abstract boolean matches(String header); + /** + * Produces a new range that matches the headers of this range and the given range. + * + * @since 2.0.0 + */ public abstract HttpHeaderRange concat(HttpHeaderRange range); public static HttpHeaderRange create(String... headers) { diff --git a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala index 6974ba684..fad2930d6 100644 --- a/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala +++ b/http-cors/src/main/scala/org/apache/pekko/http/cors/scaladsl/model/HttpHeaderRange.scala @@ -28,6 +28,11 @@ import pekko.util.Helpers sealed abstract class HttpHeaderRange extends javadsl.model.HttpHeaderRange { override def concat(range: javadsl.model.HttpHeaderRange): HttpHeaderRange + /** + * Operator alias for [[concat]]. + * + * @since 2.0.0 + */ def ++(range: javadsl.model.HttpHeaderRange): HttpHeaderRange = concat(range) }