From 05072bc59dd8b17d6e8562bd7e0887c207b400f7 Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Fri, 10 Jul 2026 14:45:19 -0300 Subject: [PATCH 1/2] Add RFC 10008 (The HTTP QUERY Method) support --- core/src/main/scala/sttp/model/Header.scala | 4 ++++ core/src/main/scala/sttp/model/HeaderNames.scala | 1 + core/src/main/scala/sttp/model/Method.scala | 5 +++-- core/src/test/scala/sttp/model/MethodTest.scala | 1 + core/src/test/scalajvm/sttp/model/HeaderTests.scala | 7 +++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/sttp/model/Header.scala b/core/src/main/scala/sttp/model/Header.scala index d2425862..ca99d3e2 100644 --- a/core/src/main/scala/sttp/model/Header.scala +++ b/core/src/main/scala/sttp/model/Header.scala @@ -80,7 +80,11 @@ object Header { def accept(mediaType: MediaType, additionalMediaTypes: MediaType*): Header = accept( s"${(mediaType :: additionalMediaTypes.toList).map(_.noCharset).mkString(", ")}" ) + def acceptQuery(mediaType: MediaType, additionalMediaTypes: MediaType*): Header = acceptQuery( + s"${(mediaType :: additionalMediaTypes.toList).map(_.toString).mkString(", ")}" + ) def accept(mediaRanges: String): Header = Header(HeaderNames.Accept, mediaRanges) + def acceptQuery(mediaRanges: String): Header = Header(HeaderNames.AcceptQuery, mediaRanges) def acceptCharset(charsetRanges: String): Header = Header(HeaderNames.AcceptCharset, charsetRanges) def acceptEncoding(encodingRanges: String): Header = Header(HeaderNames.AcceptEncoding, encodingRanges) def accessControlAllowCredentials(allow: Boolean): Header = diff --git a/core/src/main/scala/sttp/model/HeaderNames.scala b/core/src/main/scala/sttp/model/HeaderNames.scala index 9be9f690..a0086bb8 100644 --- a/core/src/main/scala/sttp/model/HeaderNames.scala +++ b/core/src/main/scala/sttp/model/HeaderNames.scala @@ -7,6 +7,7 @@ trait HeaderNames { val AcceptEncoding = "Accept-Encoding" val AcceptLanguage = "Accept-Language" val AcceptRanges = "Accept-Ranges" + val AcceptQuery = "Accept-Query" val AccessControlAllowCredentials = "Access-Control-Allow-Credentials" val AccessControlAllowHeaders = "Access-Control-Allow-Headers" val AccessControlAllowMethods = "Access-Control-Allow-Methods" diff --git a/core/src/main/scala/sttp/model/Method.scala b/core/src/main/scala/sttp/model/Method.scala index ff7f2ae1..7cbb48dd 100644 --- a/core/src/main/scala/sttp/model/Method.scala +++ b/core/src/main/scala/sttp/model/Method.scala @@ -32,8 +32,8 @@ object Method extends Methods { def isSafe(m: Method): Boolean = safe.contains(m) private val idempotent: Set[Method] = - Set(Method.HEAD, Method.TRACE, Method.GET, Method.PUT, Method.OPTIONS, Method.DELETE) - private val safe: Set[Method] = Set(Method.HEAD, Method.GET, Method.OPTIONS) + Set(Method.HEAD, Method.TRACE, Method.GET, Method.PUT, Method.OPTIONS, Method.DELETE, Method.QUERY) + private val safe: Set[Method] = Set(Method.HEAD, Method.GET, Method.OPTIONS, Method.QUERY) } trait Methods { @@ -42,6 +42,7 @@ trait Methods { val POST: Method = Method("POST") val PUT: Method = Method("PUT") val DELETE: Method = Method("DELETE") + val QUERY: Method = Method("QUERY") val OPTIONS: Method = Method("OPTIONS") val PATCH: Method = Method("PATCH") val CONNECT: Method = Method("CONNECT") diff --git a/core/src/test/scala/sttp/model/MethodTest.scala b/core/src/test/scala/sttp/model/MethodTest.scala index 3c8e682a..34788881 100644 --- a/core/src/test/scala/sttp/model/MethodTest.scala +++ b/core/src/test/scala/sttp/model/MethodTest.scala @@ -14,5 +14,6 @@ class MethodTest extends AnyFlatSpec with Matchers { Method.unsafeApply("patch") shouldBe Method.unsafeApply("PATCH") Method.unsafeApply("connect") shouldBe Method.unsafeApply("CONNECT") Method.unsafeApply("trace") shouldBe Method.unsafeApply("TRACE") + Method.unsafeApply("query") shouldBe Method.unsafeApply("QUERY") } } diff --git a/core/src/test/scalajvm/sttp/model/HeaderTests.scala b/core/src/test/scalajvm/sttp/model/HeaderTests.scala index 1400f028..41edecc2 100644 --- a/core/src/test/scalajvm/sttp/model/HeaderTests.scala +++ b/core/src/test/scalajvm/sttp/model/HeaderTests.scala @@ -52,6 +52,13 @@ class HeaderTests extends AnyFlatSpec with Matchers { .toString shouldBe "Cache-Control: no-transform, public, s-maxage=10" } + it should "properly create an accept-query header" in { + Header + .acceptQuery(MediaType.ApplicationJson, MediaType.ApplicationXml) + .toString shouldBe "Accept-Query: application/json, application/xml" + Header.acceptQuery("application/jsonpath").toString shouldBe "Accept-Query: application/jsonpath" + } + "Instant" should "be formatted according to rfc1123-date1 with a leading zero for single-digit dates" in { Header.toHttpDateString(rfc1123DatetimeToBeChecked) shouldBe rfc1123DatetimeFormatted } From a2f48a974acc67ff165c490ece5bf29067527bda Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 13 Jul 2026 11:08:34 +0000 Subject: [PATCH 2/2] Use defs instead of vals for new trait members, to maintain binary compatibility Adding a val to a trait breaks binary compatibility for external classes implementing it (the val's backing field and synthetic setter must exist in every implementor). A concrete def compiles to a JVM default method, which old implementors inherit. Co-Authored-By: Claude Fable 5 --- core/src/main/scala/sttp/model/HeaderNames.scala | 3 ++- core/src/main/scala/sttp/model/Method.scala | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/sttp/model/HeaderNames.scala b/core/src/main/scala/sttp/model/HeaderNames.scala index a0086bb8..234eff95 100644 --- a/core/src/main/scala/sttp/model/HeaderNames.scala +++ b/core/src/main/scala/sttp/model/HeaderNames.scala @@ -7,7 +7,8 @@ trait HeaderNames { val AcceptEncoding = "Accept-Encoding" val AcceptLanguage = "Accept-Language" val AcceptRanges = "Accept-Ranges" - val AcceptQuery = "Accept-Query" + // a def, not a val, as adding a val to a trait breaks binary compatibility for classes implementing it + def AcceptQuery: String = "Accept-Query" val AccessControlAllowCredentials = "Access-Control-Allow-Credentials" val AccessControlAllowHeaders = "Access-Control-Allow-Headers" val AccessControlAllowMethods = "Access-Control-Allow-Methods" diff --git a/core/src/main/scala/sttp/model/Method.scala b/core/src/main/scala/sttp/model/Method.scala index 7cbb48dd..71d91105 100644 --- a/core/src/main/scala/sttp/model/Method.scala +++ b/core/src/main/scala/sttp/model/Method.scala @@ -42,7 +42,8 @@ trait Methods { val POST: Method = Method("POST") val PUT: Method = Method("PUT") val DELETE: Method = Method("DELETE") - val QUERY: Method = Method("QUERY") + // a def, not a val, as adding a val to a trait breaks binary compatibility for classes implementing it + def QUERY: Method = Method("QUERY") val OPTIONS: Method = Method("OPTIONS") val PATCH: Method = Method("PATCH") val CONNECT: Method = Method("CONNECT")