From 820ca3ce6ae96c90412f1332bdc9b0471931c641 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 8 Jul 2024 14:17:36 +0200 Subject: [PATCH 1/4] Add tests for StreamedResponse to make sure its header's map is case insensitively (cherry picked from commit f6bac8900a9cc4a07c32ce84d91c711ceb6d3df3) --- .../scala/play/api/libs/ws/ahc/AhcWSResponseSpec.scala | 8 ++++++++ .../test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSResponseSpec.scala b/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSResponseSpec.scala index eeebc303..d44f16b1 100644 --- a/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSResponseSpec.scala +++ b/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSResponseSpec.scala @@ -128,6 +128,14 @@ class AhcWSResponseSpec extends Specification with DefaultBodyReadables with Def headers.contains("Bar") must beTrue } + "get headers map which retrieves headers case insensitively (for streamed responses)" in { + val srcHeaders = Map("Foo" -> Seq("a"), "foo" -> Seq("b"), "FOO" -> Seq("b"), "Bar" -> Seq("baz")) + val response = new StreamedResponse(null, 200, "", null, srcHeaders, null, true) + val headers = response.headers + headers("foo") must_== Seq("a", "b", "b") + headers("BAR") must_== Seq("baz") + } + "get a single header" in { val ahcResponse: AHCResponse = mock[AHCResponse] val ahcHeaders = new DefaultHttpHeaders(true) diff --git a/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala b/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala index 9a08b435..f0823d05 100644 --- a/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala +++ b/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala @@ -46,6 +46,14 @@ class AhcWSResponseSpec extends Specification with DefaultBodyReadables with Def headers.get("BAR").asScala must_== Seq("baz") } + "get headers map which retrieves headers case insensitively (for streamed responses)" in { + val srcHeaders = Map("Foo" -> Seq("a"), "foo" -> Seq("b"), "FOO" -> Seq("b"), "Bar" -> Seq("baz")) + val response = new StreamedResponse(null, 200, "", null, srcHeaders, null, true) + val headers = response.getHeaders + headers.get("foo").asScala must_== Seq("a", "b", "b") + headers.get("BAR").asScala must_== Seq("baz") + } + "get a single header" in { val srcResponse = mock[Response] val srcHeaders = new DefaultHttpHeaders() From f74d04444f1f3c62c527b2911bab0ef3f7d3a8f4 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 8 Jul 2024 14:22:47 +0200 Subject: [PATCH 2/4] Make Java's StreamedResponse.headers case insensitively (cherry picked from commit a79f60853497a609b77aeb916adb86c550d37414) --- .../java/play/libs/ws/ahc/StreamedResponse.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StreamedResponse.java b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StreamedResponse.java index d7120c5b..c2d43951 100644 --- a/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StreamedResponse.java +++ b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StreamedResponse.java @@ -17,9 +17,11 @@ import scala.jdk.javaapi.StreamConverters; import java.net.URI; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import java.util.function.Predicate; import static java.util.stream.Collectors.toMap; @@ -121,7 +123,16 @@ public URI getUri() { } private static java.util.Map> asJava(scala.collection.Map> scalaMap) { - return StreamConverters.asJavaSeqStream(scalaMap).collect(toMap(f -> f._1(), f -> CollectionConverters.asJava(f._2()))); + return StreamConverters.asJavaSeqStream(scalaMap).collect(toMap(f -> f._1(), f -> CollectionConverters.asJava(f._2()), + (l, r) -> { + final List merged = new ArrayList<>(l.size() + r.size()); + merged.addAll(l); + merged.addAll(r); + return merged; + }, + () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER) + ) + ); } } From 5db019638fc48a3a45d542b968d89345915008d1 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 8 Jul 2024 14:35:16 +0200 Subject: [PATCH 3/4] Make Scala's StreamedResponse.headers case insensitively (cherry picked from commit d2a07f3ab1b84764eef43ba2b65ef707800c95e3) --- .../play/api/libs/ws/ahc/StreamedResponse.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala index 04a8e812..d12ecd8f 100644 --- a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala +++ b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala @@ -11,6 +11,9 @@ import play.api.libs.ws.StandaloneWSResponse import play.api.libs.ws.WSCookie import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart +import scala.collection.immutable.TreeMap +import scala.collection.mutable + /** * A streamed response containing a response header and a streamable body. * @@ -39,7 +42,7 @@ class StreamedResponse( val status: Int, val statusText: String, val uri: java.net.URI, - val headers: Map[String, scala.collection.Seq[String]], + headers: Map[String, scala.collection.Seq[String]], publisher: Publisher[HttpResponseBodyPart], val useLaxCookieEncoder: Boolean ) extends StandaloneWSResponse @@ -50,6 +53,17 @@ class StreamedResponse( */ override def underlying[T]: T = publisher.asInstanceOf[T] + override def headers(): Map[String, scala.collection.Seq[String]] = { + val mutableMap = mutable.TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) + headers.keys.foreach { name => + mutableMap.updateWith(name) { + case Some(value) => Some(value ++ headers.getOrElse(name, Seq.empty)) + case None => Some(headers.getOrElse(name, Seq.empty)) + } + } + TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) ++ mutableMap + } + /** * Get all the cookies. */ From b9799ae261b1277bdad62fb0f3e87add6b76b67c Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 8 Jul 2024 14:47:45 +0200 Subject: [PATCH 4/4] Can't override headers() in Scala 3 + stay compatible (cherry picked from commit 329a4bc596ab5f7e6228a319ffe3783597d7dd14) --- .../api/libs/ws/ahc/StreamedResponse.scala | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala index d12ecd8f..02489395 100644 --- a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala +++ b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala @@ -42,23 +42,44 @@ class StreamedResponse( val status: Int, val statusText: String, val uri: java.net.URI, - headers: Map[String, scala.collection.Seq[String]], publisher: Publisher[HttpResponseBodyPart], val useLaxCookieEncoder: Boolean ) extends StandaloneWSResponse with CookieBuilder { + def this( + client: StandaloneAhcWSClient, + status: Int, + statusText: String, + uri: java.net.URI, + headers: Map[String, scala.collection.Seq[String]], + publisher: Publisher[HttpResponseBodyPart], + useLaxCookieEncoder: Boolean + ) = { + this( + client, + status, + statusText, + uri, + publisher, + useLaxCookieEncoder + ) + origHeaders = headers + } + + private var origHeaders: Map[String, scala.collection.Seq[String]] = Map.empty + /** * Get the underlying response object. */ override def underlying[T]: T = publisher.asInstanceOf[T] - override def headers(): Map[String, scala.collection.Seq[String]] = { + override lazy val headers: Map[String, scala.collection.Seq[String]] = { val mutableMap = mutable.TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) - headers.keys.foreach { name => + origHeaders.keys.foreach { name => mutableMap.updateWith(name) { - case Some(value) => Some(value ++ headers.getOrElse(name, Seq.empty)) - case None => Some(headers.getOrElse(name, Seq.empty)) + case Some(value) => Some(value ++ origHeaders.getOrElse(name, Seq.empty)) + case None => Some(origHeaders.getOrElse(name, Seq.empty)) } } TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) ++ mutableMap