From 6df05df77c86ff61b2a08131b430472758f820b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Fri, 19 Jun 2026 12:51:18 +0800 Subject: [PATCH 1/5] feat: add RFC 10008 QUERY method and Accept-Query header support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: RFC 10008 defines the HTTP QUERY method as a safe, idempotent method that expects a request body, along with the Accept-Query response header for signaling supported query media types. pekko-http should provide first-class support for this standard HTTP method. Modification: - Add QUERY method constant to HttpMethods (Scala and Java API) - Add Accept-Query modeled response header with MediaRange list - Add Accept-Query parser supporting RFC 9651 Structured Fields syntax (quoted strings like "application/jsonpath") with q-value stripping - Add query routing directive (Scala DSL and Java DSL) - Add HttpRequest.QUERY factory method (Java API) - Add AcceptQuery Java API abstract class - Register accept-query in HeaderParser dispatch - Add documentation page for query directive with Scala/Java examples - Add directional tests: method lookup, header parsing (token, quoted string, wildcard, q-value stripping), directive routing Result: Downstream consumers can implement RFC 10008 QUERY method support using built-in constants, headers, parsing, and routing directives. Tests: - sbt "http-core / Test / testOnly org.apache.pekko.http.scaladsl.model.HttpMethodsSpec" → 6 passed - sbt "http-core / Test / testOnly org.apache.pekko.http.impl.model.parser.HttpHeaderSpec" → 75 passed - sbt "http-core / Test / testOnly org.apache.pekko.http.scaladsl.model.headers.HeaderSpec" → 18 passed - sbt "http-tests / Test / testOnly org.apache.pekko.http.scaladsl.server.directives.MethodDirectivesSpec" → 14 passed - sbt "docs / Test / testOnly docs.http.scaladsl.server.directives.MethodDirectivesExamplesSpec docs.http.javadsl.server.directives.MethodDirectivesExamplesTest" → 11 passed - sbt "http-core / mimaReportBinaryIssues" → success - sbt "http / mimaReportBinaryIssues" → success References: Refs https://github.com/netty/netty/pull/16978, https://www.rfc-editor.org/rfc/rfc10008 --- .../directives/method-directives/index.md | 1 + .../directives/method-directives/query.md | 31 ++++++++++++++++ .../MethodDirectivesExamplesTest.java | 16 +++++++++ .../MethodDirectivesExamplesSpec.scala | 11 ++++++ .../pekko/http/javadsl/model/HttpMethods.java | 1 + .../pekko/http/javadsl/model/HttpRequest.java | 5 +++ .../javadsl/model/headers/AcceptQuery.java | 27 ++++++++++++++ .../http/impl/model/parser/AcceptHeader.scala | 36 +++++++++++++++++++ .../http/impl/model/parser/HeaderParser.scala | 1 + .../http/scaladsl/model/HttpMethod.scala | 1 + .../http/scaladsl/model/headers/headers.scala | 16 +++++++++ .../impl/model/parser/HttpHeaderSpec.scala | 33 +++++++++++++++++ .../http/scaladsl/model/HttpMethodsSpec.scala | 3 ++ .../scaladsl/model/headers/HeaderSpec.scala | 1 + .../directives/MethodDirectivesSpec.scala | 13 ++++++- .../server/directives/MethodDirectives.scala | 4 +++ .../server/directives/MethodDirectives.scala | 8 +++++ 17 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/paradox/routing-dsl/directives/method-directives/query.md create mode 100644 http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java diff --git a/docs/src/main/paradox/routing-dsl/directives/method-directives/index.md b/docs/src/main/paradox/routing-dsl/directives/method-directives/index.md index 3b12d24ec4..a809bb4c15 100644 --- a/docs/src/main/paradox/routing-dsl/directives/method-directives/index.md +++ b/docs/src/main/paradox/routing-dsl/directives/method-directives/index.md @@ -14,5 +14,6 @@ * [patch](patch.md) * [post](post.md) * [put](put.md) +* [query](query.md) @@@ \ No newline at end of file diff --git a/docs/src/main/paradox/routing-dsl/directives/method-directives/query.md b/docs/src/main/paradox/routing-dsl/directives/method-directives/query.md new file mode 100644 index 0000000000..839837ca61 --- /dev/null +++ b/docs/src/main/paradox/routing-dsl/directives/method-directives/query.md @@ -0,0 +1,31 @@ +# query + +Matches requests with HTTP method `QUERY` (RFC 10008). + +@@@ div { .group-scala } + +## Signature + +@@signature [MethodDirectives.scala](/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala) { #query } + +@@@ + +## Description + +This directive filters the incoming request by its HTTP method. Only requests with +method `QUERY` are passed on to the inner route. All others are rejected with a +@apidoc[MethodRejection], which is translated into a `405 Method Not Allowed` response +by the default @ref[RejectionHandler](../../rejections.md#the-rejectionhandler). + +The `QUERY` method is defined in [RFC 10008](https://www.rfc-editor.org/rfc/rfc10008). +It is a safe and idempotent method that requests the target resource to process the +enclosed content and respond with the result. Unlike `GET`, the `QUERY` method expects +a request body containing the query payload. + +## Example + +Scala +: @@snip [MethodDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala) { #query-method } + +Java +: @@snip [MethodDirectivesExamplesTest.java](/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java) { #query } diff --git a/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java b/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java index 3c577f34f6..5090d2f5a3 100644 --- a/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java +++ b/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java @@ -51,6 +51,11 @@ import static org.apache.pekko.http.javadsl.server.Directives.put; // #put +// #query +import static org.apache.pekko.http.javadsl.server.Directives.complete; +import static org.apache.pekko.http.javadsl.server.Directives.query; + +// #query // #method-example import static org.apache.pekko.http.javadsl.server.Directives.complete; import static org.apache.pekko.http.javadsl.server.Directives.method; @@ -140,6 +145,17 @@ public void testPut() { // #put } + @Test + public void testQuery() { + // #query + final Route route = query(() -> complete("This is a QUERY request.")); + + testRoute(route) + .run(HttpRequest.QUERY("/").withEntity("query content")) + .assertEntity("This is a QUERY request."); + // #query + } + @Test public void testMethodExample() { // #method-example diff --git a/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala b/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala index cb03e4f961..c2a2565681 100644 --- a/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala +++ b/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala @@ -98,6 +98,17 @@ class MethodDirectivesExamplesSpec extends RoutingSpec with CompileOnlySpec { // #put-method } + "query-method" in { + // #query-method + val route = query { complete("This is a QUERY request.") } + + // tests: + HttpRequest(method = HttpMethods.QUERY, uri = "/") ~> route ~> check { + responseAs[String] shouldEqual "This is a QUERY request." + } + // #query-method + } + "method-example" in { // #method-example val route = method(HttpMethods.PUT) { complete("This is a PUT request.") } diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMethods.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMethods.java index b39bdf8464..73f8982147 100644 --- a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMethods.java +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMethods.java @@ -31,6 +31,7 @@ private HttpMethods() {} public static final HttpMethod PATCH = org.apache.pekko.http.scaladsl.model.HttpMethods.PATCH(); public static final HttpMethod POST = org.apache.pekko.http.scaladsl.model.HttpMethods.POST(); public static final HttpMethod PUT = org.apache.pekko.http.scaladsl.model.HttpMethods.PUT(); + public static final HttpMethod QUERY = org.apache.pekko.http.scaladsl.model.HttpMethods.QUERY(); public static final HttpMethod TRACE = org.apache.pekko.http.scaladsl.model.HttpMethods.TRACE(); /** diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpRequest.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpRequest.java index 8d82d87c92..b5b3d0f22b 100644 --- a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpRequest.java +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpRequest.java @@ -83,4 +83,9 @@ public static HttpRequest PATCH(String uri) { public static HttpRequest OPTIONS(String uri) { return create(uri).withMethod(HttpMethods.OPTIONS); } + + /** A default QUERY request to be modified using the `withX` methods. */ + public static HttpRequest QUERY(String uri) { + return create(uri).withMethod(HttpMethods.QUERY); + } } diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java new file mode 100644 index 0000000000..50daf185d4 --- /dev/null +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * license agreements; and to You under the Apache License, version 2.0: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * This file is part of the Apache Pekko project, which was derived from Akka. + */ + +package org.apache.pekko.http.javadsl.model.headers; + +import org.apache.pekko.http.impl.util.Util; +import org.apache.pekko.http.javadsl.model.MediaRange; + +/** + * Model for the `Accept-Query` header. Specification: + * https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 + */ +public abstract class AcceptQuery extends org.apache.pekko.http.scaladsl.model.HttpHeader { + public abstract Iterable getMediaRanges(); + + public static AcceptQuery create(MediaRange... mediaRanges) { + return new org.apache.pekko.http.scaladsl.model.headers.Accept$minusQuery( + Util.convertArray( + mediaRanges)); + } +} diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala index c38f79aa2f..03c9ae6cc4 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala @@ -29,6 +29,42 @@ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonAc zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_)) } + // https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 + // Accept-Query uses Structured Fields syntax (RFC 9651): media types may appear + // as tokens (application/json) or quoted strings ("application/jsonpath"). + // Unlike Accept, q-values are not meaningful and are stripped if present. + def `accept-query` = rule { + zeroOrMore(`accept-query-media-range-decl`).separatedBy(listSep) ~ EOI ~> (`Accept-Query`(_)) + } + + def `accept-query-media-range-decl` = rule { + `accept-query-media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params) => + val cleanParams = TreeMap(params.filterNot(_._1 == "q"): _*) + if (sub == "*") { + val mainLower = main.toRootLowerCase + MediaRanges.getForKey(mainLower) match { + case Some(registered) => if (cleanParams.isEmpty) registered else registered.withParams(cleanParams) + case None => MediaRange.custom(mainLower, cleanParams) + } + } else { + MediaRange(getMediaType(main, sub, cleanParams contains "charset", cleanParams)) + } + } + } + + def `accept-query-media-range-def` = rule { + "*/*" ~ push("*") ~ push("*") | + '*' ~ push("*") ~ push("*") | + `type` ~ '/' ~ + ('*' ~ !tchar ~ push("*") | subtype) | + `quoted-string` ~> + ((s: String) => { + val slashIdx = s.indexOf('/') + if (slashIdx > 0) push(s.substring(0, slashIdx)) ~ push(s.substring(slashIdx + 1)) + else push(s) ~ push("*") + }) + } + def `media-range-decl` = rule { `media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params) => if (sub == "*") { diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/HeaderParser.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/HeaderParser.scala index e82c6aafca..870da7aa79 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/HeaderParser.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/HeaderParser.scala @@ -144,6 +144,7 @@ private[http] object HeaderParser { "accept-charset", "accept-encoding", "accept-language", + "accept-query", "accept-ranges", "access-control-allow-credentials", "access-control-allow-headers", diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMethod.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMethod.scala index a00caae9b8..d407986241 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMethod.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMethod.scala @@ -114,6 +114,7 @@ object HttpMethods extends ObjectRegistry[String, HttpMethod] { val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon)) val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon)) val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon)) + val QUERY = register(HttpMethod("QUERY" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon)) val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Disallowed, contentLengthAllowed = contentLengthAllowedCommon)) // format: ON diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala index e8c1c716d7..b5aa644e35 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala @@ -232,6 +232,22 @@ final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends j def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava } +// https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 +object `Accept-Query` extends ModeledCompanion[`Accept-Query`] { + def apply(firstMediaRange: MediaRange, otherMediaRanges: MediaRange*): `Accept-Query` = + apply(firstMediaRange +: otherMediaRanges) + implicit val mediaRangesRenderer: Renderer[immutable.Iterable[MediaRange]] = Renderer.defaultSeqRenderer[MediaRange] // cache +} +final case class `Accept-Query`(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.AcceptQuery + with ResponseHeader { + import `Accept-Query`.mediaRangesRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges + protected def companion = `Accept-Query` + + /** Java API */ + def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava +} + // https://www.w3.org/TR/cors/#access-control-allow-credentials-response-header object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`] final case class `Access-Control-Allow-Credentials`(allow: Boolean) diff --git a/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala index 644d0f075d..54caff1c71 100644 --- a/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala +++ b/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala @@ -120,6 +120,39 @@ class HttpHeaderSpec extends AnyFreeSpec with Matchers { "Accept-Ranges: none" =!= `Accept-Ranges`() } + "Accept-Query" in { + // basic token form + "Accept-Query: application/json" =!= `Accept-Query`(MediaTypes.`application/json`) + // multiple media ranges + "Accept-Query: application/json, text/xml" =!= + `Accept-Query`(MediaTypes.`application/json`, MediaTypes.`text/xml`) + // full wildcard + "Accept-Query: */*" =!= `Accept-Query`(MediaRanges.`*/*`) + // type wildcard + "Accept-Query: application/*" =!= `Accept-Query`(MediaRanges.`application/*`) + // RFC 9651 Structured Fields: quoted string form (application/jsonpath is not predefined) + """Accept-Query: "application/jsonpath"""" =!= + `Accept-Query`(MediaType.customBinary("application", "jsonpath", MediaType.Compressible, + allowArbitrarySubtypes = true)) + .renderedTo("application/jsonpath") + // RFC 10008 Section 3.2 example: mixed token and quoted-string forms with parameters + """Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"""" =!= + `Accept-Query`( + MediaType.customBinary("application", "jsonpath", MediaType.Compressible, + allowArbitrarySubtypes = true), + MediaType.customBinary("application", "sql", MediaType.Compressible, + params = Map("charset" -> "UTF-8"), allowArbitrarySubtypes = true)) + .renderedTo("application/jsonpath, application/sql; charset=UTF-8") + // q-values are not meaningful for Accept-Query (RFC 10008) and must be stripped + "Accept-Query: application/json;q=0.8" =!= + `Accept-Query`(MediaTypes.`application/json`) + .renderedTo("application/json") + // q-values stripped even with other parameters preserved + "Accept-Query: application/json;charset=utf-8;q=0.5" =!= + `Accept-Query`(MediaTypes.`application/json`.withParams(Map("charset" -> "utf-8"))) + .renderedTo("application/json; charset=utf-8") + } + "Accept-Encoding" in { "Accept-Encoding: compress, gzip, fancy" =!= `Accept-Encoding`(compress, gzip, HttpEncoding.custom("fancy")) diff --git a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala index 16a198e8de..873860baae 100644 --- a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala +++ b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala @@ -32,5 +32,8 @@ class HttpMethodsSpec extends AnyWordSpec { "return HttpMethods.OPTIONS" in { assert(HttpMethods.getForKeyCaseInsensitive("oPtIoNs") == Option(HttpMethods.OPTIONS)) } + "return HttpMethods.QUERY" in { + assert(HttpMethods.getForKeyCaseInsensitive("query") == Option(HttpMethods.QUERY)) + } } } diff --git a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/headers/HeaderSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/headers/HeaderSpec.scala index 73424fbb7a..f5af11aff3 100644 --- a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/headers/HeaderSpec.scala +++ b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/headers/HeaderSpec.scala @@ -206,6 +206,7 @@ class HeaderSpec extends AnyFreeSpec with Matchers { "render in response" in { val responseHeaders = Vector[HttpHeader]( `Accept-Ranges`(RangeUnits.Bytes), + `Accept-Query`(MediaTypes.`application/json`), `Access-Control-Allow-Credentials`(true), `Access-Control-Allow-Headers`("X-Custom"), `Access-Control-Allow-Methods`(HttpMethods.GET), diff --git a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala index 3d3d72caeb..36956580de 100644 --- a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala +++ b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala @@ -14,7 +14,7 @@ package org.apache.pekko.http.scaladsl.server.directives import org.apache.pekko -import pekko.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpMethods, StatusCodes } +import pekko.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpMethod, HttpMethods, HttpRequest, StatusCodes } import pekko.http.scaladsl.server._ import pekko.stream.scaladsl.Source @@ -53,6 +53,17 @@ class MethodDirectivesSpec extends RoutingSpec { } } + "query" should { + lazy val queryRoute = query { completeOk } + + "block GET requests" in { + Get() ~> queryRoute ~> check { handled shouldEqual false } + } + "let QUERY requests pass" in { + HttpRequest(method = HttpMethods.QUERY) ~> queryRoute ~> check { response shouldEqual Ok } + } + } + "two failed `get` directives" should { "only result in a single Rejection" in { Put() ~> { diff --git a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MethodDirectives.scala b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MethodDirectives.scala index 871d835fae..d5b69f9e3c 100644 --- a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MethodDirectives.scala +++ b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MethodDirectives.scala @@ -47,6 +47,10 @@ abstract class MethodDirectives extends MarshallingDirectives { D.put { inner.get.delegate } } + def query(inner: function.Supplier[Route]): Route = RouteAdapter { + D.query { inner.get.delegate } + } + def extractMethod(inner: function.Function[HttpMethod, Route]) = RouteAdapter { D.extractMethod { m => inner.apply(m).delegate 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 01ca61ea71..127659ead7 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 @@ -77,6 +77,13 @@ trait MethodDirectives { */ def put: Directive0 = _put + /** + * Rejects all non-QUERY requests. + * + * @group method + */ + def query: Directive0 = _query + /** * Extracts the request method. * @@ -131,5 +138,6 @@ object MethodDirectives extends MethodDirectives { private val _patch : Directive0 = method(PATCH) private val _post : Directive0 = method(POST) private val _put : Directive0 = method(PUT) + private val _query : Directive0 = method(QUERY) // format: ON } From 51c77fc5aac96ba775cb6f16550d81393e5b777b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Fri, 19 Jun 2026 12:58:07 +0800 Subject: [PATCH 2/5] test: add RFC 10008 QUERY method property tests and clean up imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Review feedback identified missing directional tests for QUERY method semantics (safe, idempotent, expected entity) and an unused import. Modification: - Add property tests verifying QUERY is safe, idempotent, and expects a request entity per RFC 10008 (goes beyond Netty's test coverage) - Remove unused HttpMethod import from MethodDirectivesSpec Result: Stronger test coverage ensuring RFC 10008 semantics are preserved. Tests: - sbt "http-core / Test / testOnly org.apache.pekko.http.scaladsl.model.HttpMethodsSpec" → 9 passed - sbt "http-tests / Test / testOnly org.apache.pekko.http.scaladsl.server.directives.MethodDirectivesSpec" → 14 passed References: Refs https://www.rfc-editor.org/rfc/rfc10008 --- .../pekko/http/scaladsl/model/HttpMethodsSpec.scala | 12 ++++++++++++ .../server/directives/MethodDirectivesSpec.scala | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala index 873860baae..f60dd55cb2 100644 --- a/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala +++ b/http-core/src/test/scala/org/apache/pekko/http/scaladsl/model/HttpMethodsSpec.scala @@ -36,4 +36,16 @@ class HttpMethodsSpec extends AnyWordSpec { assert(HttpMethods.getForKeyCaseInsensitive("query") == Option(HttpMethods.QUERY)) } } + + "HttpMethods.QUERY" must { + "be safe per RFC 10008" in { + assert(HttpMethods.QUERY.isSafe) + } + "be idempotent per RFC 10008" in { + assert(HttpMethods.QUERY.isIdempotent) + } + "expect a request entity per RFC 10008" in { + assert(HttpMethods.QUERY.requestEntityAcceptance == RequestEntityAcceptance.Expected) + } + } } diff --git a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala index 36956580de..e6ec218b36 100644 --- a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala +++ b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectivesSpec.scala @@ -14,7 +14,7 @@ package org.apache.pekko.http.scaladsl.server.directives import org.apache.pekko -import pekko.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpMethod, HttpMethods, HttpRequest, StatusCodes } +import pekko.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpMethods, HttpRequest, StatusCodes } import pekko.http.scaladsl.server._ import pekko.stream.scaladsl.Source From 98f8f94767b1be812f77bdb045432171d5fc5704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Fri, 19 Jun 2026 16:11:04 +0800 Subject: [PATCH 3/5] Align Accept-Query with RFC 10008 Motivation: Accept-Query is a Structured Field in RFC 10008, but the PR reused Accept media-range parsing and rendering in places that could emit invalid field values or treat parameters as Accept weights. Modification: Parse Accept-Query media ranges and parameters with Structured Field token/string rules. Render Accept-Query values with token-or-string selection, preserve valid SF parameters, reject invalid wildcard and decimal-parameter forms, and add focused parser/rendering tests. Add the query directive to the alphabetical docs index and fix Accept-Query RFC section links. Result: Accept-Query parsing and rendering now matches RFC 10008 Section 3 while keeping the parser path lightweight. Tests: - scalafmt --mode diff-ref=origin/main - scalafmt --list --mode diff-ref=origin/main - git diff --check - sbt 'project http-core' 'Test / testOnly org.apache.pekko.http.impl.model.parser.HttpHeaderSpec' - sbt 'project http-core' 'Test / testOnly org.apache.pekko.http.scaladsl.model.headers.HeaderSpec' - sbt 'project http-core' 'Test / testOnly org.apache.pekko.http.scaladsl.model.HttpMethodsSpec' - sbt 'http-tests / Test / testOnly org.apache.pekko.http.scaladsl.server.directives.MethodDirectivesSpec' - sbt docs/paradox References: Refs #1077 --- .../routing-dsl/directives/alphabetically.md | 1 + .../javadsl/model/headers/AcceptQuery.java | 2 +- .../http/impl/model/parser/AcceptHeader.scala | 97 ++++++++++++++----- .../http/scaladsl/model/MediaRange.scala | 4 + .../http/scaladsl/model/headers/headers.scala | 42 +++++++- .../impl/model/parser/HttpHeaderSpec.scala | 45 ++++++--- 6 files changed, 151 insertions(+), 40 deletions(-) diff --git a/docs/src/main/paradox/routing-dsl/directives/alphabetically.md b/docs/src/main/paradox/routing-dsl/directives/alphabetically.md index db9d59d201..7bcb5896be 100644 --- a/docs/src/main/paradox/routing-dsl/directives/alphabetically.md +++ b/docs/src/main/paradox/routing-dsl/directives/alphabetically.md @@ -127,6 +127,7 @@ |@ref[post](method-directives/post.md) | Rejects all non-POST requests | |@ref[provide](basic-directives/provide.md) | Injects a given value into a directive | |@ref[put](method-directives/put.md) | Rejects all non-PUT requests | +|@ref[query](method-directives/query.md) | Rejects all non-QUERY requests | |@ref[rawPathPrefix](path-directives/rawPathPrefix.md) | Applies the given matcher directly to a prefix of the unmatched path of the @apidoc[RequestContext], without implicitly consuming a leading slash | |@ref[rawPathPrefixTest](path-directives/rawPathPrefixTest.md) | Checks whether the unmatchedPath has a prefix matched by the given `PathMatcher` | |@ref[recoverRejections](basic-directives/recoverRejections.md) | Transforms rejections from the inner route with an @scala[`immutable.Seq[Rejection] => RouteResult`]@java[`Function, RouteResult>`] function | diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java index 50daf185d4..18c3b8bf49 100644 --- a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java @@ -14,7 +14,7 @@ /** * Model for the `Accept-Query` header. Specification: - * https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 + * https://www.rfc-editor.org/rfc/rfc10008.html#section-3 */ public abstract class AcceptQuery extends org.apache.pekko.http.scaladsl.model.HttpHeader { public abstract Iterable getMediaRanges(); diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala index 03c9ae6cc4..e859693080 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala @@ -16,55 +16,104 @@ package org.apache.pekko.http.impl.model.parser import scala.collection.immutable.TreeMap import org.apache.pekko -import org.parboiled2.Parser +import org.parboiled2.{ CharPredicate, Parser, Rule1 } import pekko.http.scaladsl.model.headers._ -import pekko.http.scaladsl.model.{ MediaRange, MediaRanges } +import pekko.http.scaladsl.model.{ MediaRange, MediaRanges, ParsingException } import pekko.http.impl.util._ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonActions => import CharacterClasses._ + private[this] val acceptQueryMediaRangeChar = tchar ++ '/' + private[this] val sfStringChar = CharPredicate('\u0020' to '\u0021', '\u0023' to '\u005B', '\u005D' to '\u007E') + private[this] val sfStringEscapedChar = CharPredicate('"', '\\') + // http://tools.ietf.org/html/rfc7231#section-5.3.2 def accept = rule { zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_)) } - // https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 + // https://www.rfc-editor.org/rfc/rfc10008.html#section-3 // Accept-Query uses Structured Fields syntax (RFC 9651): media types may appear // as tokens (application/json) or quoted strings ("application/jsonpath"). - // Unlike Accept, q-values are not meaningful and are stripped if present. def `accept-query` = rule { zeroOrMore(`accept-query-media-range-decl`).separatedBy(listSep) ~ EOI ~> (`Accept-Query`(_)) } def `accept-query-media-range-decl` = rule { - `accept-query-media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params) => - val cleanParams = TreeMap(params.filterNot(_._1 == "q"): _*) - if (sub == "*") { - val mainLower = main.toRootLowerCase - MediaRanges.getForKey(mainLower) match { - case Some(registered) => if (cleanParams.isEmpty) registered else registered.withParams(cleanParams) - case None => MediaRange.custom(mainLower, cleanParams) + `accept-query-media-range-def` ~ zeroOrMore(`accept-query-param`) ~> { + (mediaRange: (String, String), params: Seq[(String, String)]) => + val (main, sub) = mediaRange + val mediaRangeParams = TreeMap(params: _*) + if (sub == "*") { + val mainLower = main.toRootLowerCase + MediaRanges.getForKey(mainLower) match { + case Some(registered) => + if (mediaRangeParams.isEmpty) registered else MediaRange.customWithParams(mainLower, mediaRangeParams) + case None => MediaRange.customWithParams(mainLower, mediaRangeParams) + } + } else { + MediaRange(getMediaType(main, sub, mediaRangeParams contains "charset", mediaRangeParams)) } - } else { - MediaRange(getMediaType(main, sub, cleanParams contains "charset", cleanParams)) - } } } def `accept-query-media-range-def` = rule { - "*/*" ~ push("*") ~ push("*") | - '*' ~ push("*") ~ push("*") | - `type` ~ '/' ~ - ('*' ~ !tchar ~ push("*") | subtype) | - `quoted-string` ~> - ((s: String) => { - val slashIdx = s.indexOf('/') - if (slashIdx > 0) push(s.substring(0, slashIdx)) ~ push(s.substring(slashIdx + 1)) - else push(s) ~ push("*") - }) + `sf-token` ~> (parseAcceptQueryMediaRange _) | + `sf-string` ~> (parseAcceptQueryMediaRange _) + } + + def `accept-query-param` = rule { + ';' ~ OWS ~ `sf-key` ~ '=' ~ `sf-param-value` ~> ((_, _)) + } + + def `sf-param-value`: Rule1[String] = rule { + `sf-string` | `sf-token` + } + + def `sf-key`: Rule1[String] = rule { + capture((LOWER_ALPHA | '*') ~ zeroOrMore(LOWER_ALPHA | DIGIT | '_' | '-' | '.' | '*')) + } + + def `sf-token`: Rule1[String] = rule { + capture((ALPHA | '*') ~ zeroOrMore(tchar | ':' | '/')) ~ OWS + } + + def `sf-string`: Rule1[String] = rule { + DQUOTE ~ clearSB() ~ zeroOrMore(`sf-string-char` ~ appendSB() | '\\' ~ `sf-string-escaped-char` ~ appendSB()) ~ + push(sb.toString) ~ DQUOTE ~ OWS + } + + def `sf-string-char` = rule { sfStringChar } + + def `sf-string-escaped-char` = rule { sfStringEscapedChar } + + private def parseAcceptQueryMediaRange(value: String): (String, String) = { + var slashIdx = -1 + var ix = 0 + while (ix < value.length) { + val ch = value.charAt(ix) + if (ch == '/') { + if (slashIdx >= 0) invalidAcceptQueryMediaRange(value) + slashIdx = ix + } else if (!acceptQueryMediaRangeChar(ch)) invalidAcceptQueryMediaRange(value) + ix += 1 + } + + if (slashIdx <= 0 || slashIdx == value.length - 1) invalidAcceptQueryMediaRange(value) + + val main = value.substring(0, slashIdx) + val sub = value.substring(slashIdx + 1) + if (main.indexOf('*') >= 0 && main != "*") invalidAcceptQueryMediaRange(value) + if (sub.indexOf('*') >= 0 && sub != "*") invalidAcceptQueryMediaRange(value) + if (main == "*" && sub != "*") invalidAcceptQueryMediaRange(value) + + (main, sub) } + private def invalidAcceptQueryMediaRange(value: String): Nothing = + throw ParsingException(s"Illegal Accept-Query media range '$value'") + def `media-range-decl` = rule { `media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params) => if (sub == "*") { diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaRange.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaRange.scala index 2352d3a817..311819f8f3 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaRange.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaRange.scala @@ -94,6 +94,10 @@ object MediaRange { Custom(mainType.toRootLowerCase, ps, q) } + private[http] def customWithParams(mainType: String, params: Map[String, String], + qValue: Float = 1.0f): MediaRange = + Custom(mainType.toRootLowerCase, params, qValue) + final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") def mainType = mediaType.mainType diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala index b5aa644e35..3f13483cc4 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala @@ -27,9 +27,11 @@ import scala.reflect.ClassTag import scala.util.{ Failure, Success, Try } import scala.annotation.tailrec import scala.collection.immutable +import org.parboiled2.CharPredicate import org.parboiled2.util.Base64 import pekko.event.Logging import pekko.http.impl.util._ +import pekko.http.impl.model.parser.CharacterClasses import pekko.http.impl.model.parser.CharacterClasses.`attr-char` import pekko.http.javadsl.{ model => jm } import pekko.http.scaladsl.model._ @@ -232,11 +234,49 @@ final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends j def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava } -// https://www.rfc-editor.org/rfc/rfc10008.html#section-4.1 +// https://www.rfc-editor.org/rfc/rfc10008.html#section-3 object `Accept-Query` extends ModeledCompanion[`Accept-Query`] { def apply(firstMediaRange: MediaRange, otherMediaRanges: MediaRange*): `Accept-Query` = apply(firstMediaRange +: otherMediaRanges) + + private[this] val sfTokenChar: CharPredicate = CharacterClasses.tchar ++ ":/" + + private implicit val mediaRangeRenderer: Renderer[MediaRange] = new Renderer[MediaRange] { + def render[R <: Rendering](r: R, mediaRange: MediaRange): r.type = { + renderSfTokenOrString(r, mediaRangeValue(mediaRange)) + mediaRange.params.foreach { + case (key, value) => + r ~~ ';' ~~ key.toRootLowerCase ~~ '=' + renderSfTokenOrString(r, value) + } + r + } + } + implicit val mediaRangesRenderer: Renderer[immutable.Iterable[MediaRange]] = Renderer.defaultSeqRenderer[MediaRange] // cache + + private def mediaRangeValue(mediaRange: MediaRange): String = + mediaRange match { + case MediaRange.One(mediaType, _) => mediaType.mainType + '/' + mediaType.subType + case _ => mediaRange.mainType + "/*" + } + + private def renderSfTokenOrString[R <: Rendering](r: R, value: String): r.type = + if (isSfToken(value)) r ~~ value else r ~~#! value + + private def isSfToken(value: String): Boolean = + value.nonEmpty && isSfTokenStart(value.charAt(0)) && { + var ix = 1 + var valid = true + while (valid && ix < value.length) { + valid = sfTokenChar(value.charAt(ix)) + ix += 1 + } + valid + } + + private def isSfTokenStart(ch: Char): Boolean = + CharacterClasses.ALPHA(ch) || ch == '*' } final case class `Accept-Query`(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.AcceptQuery with ResponseHeader { diff --git a/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala b/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala index 54caff1c71..a7a8658be3 100644 --- a/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala +++ b/http-core/src/test/scala/org/apache/pekko/http/impl/model/parser/HttpHeaderSpec.scala @@ -121,6 +121,7 @@ class HttpHeaderSpec extends AnyFreeSpec with Matchers { } "Accept-Query" in { + "Accept-Query: " =!= `Accept-Query`(Seq.empty[MediaRange]) // basic token form "Accept-Query: application/json" =!= `Accept-Query`(MediaTypes.`application/json`) // multiple media ranges @@ -131,26 +132,42 @@ class HttpHeaderSpec extends AnyFreeSpec with Matchers { // type wildcard "Accept-Query: application/*" =!= `Accept-Query`(MediaRanges.`application/*`) // RFC 9651 Structured Fields: quoted string form (application/jsonpath is not predefined) + val jsonPath = MediaType.customBinary("application", "jsonpath", MediaType.Compressible, + allowArbitrarySubtypes = true) """Accept-Query: "application/jsonpath"""" =!= - `Accept-Query`(MediaType.customBinary("application", "jsonpath", MediaType.Compressible, - allowArbitrarySubtypes = true)) - .renderedTo("application/jsonpath") + `Accept-Query`(jsonPath).renderedTo("application/jsonpath") + // RFC 10008 requires the string form for media ranges that are not valid Structured Field tokens + val leadingDigit = MediaType.customBinary("3", "example", MediaType.Compressible, allowArbitrarySubtypes = true) + """Accept-Query: "3/example"""" =!= + `Accept-Query`(leadingDigit).renderedTo(""""3/example"""") // RFC 10008 Section 3.2 example: mixed token and quoted-string forms with parameters """Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"""" =!= `Accept-Query`( - MediaType.customBinary("application", "jsonpath", MediaType.Compressible, - allowArbitrarySubtypes = true), + jsonPath, MediaType.customBinary("application", "sql", MediaType.Compressible, params = Map("charset" -> "UTF-8"), allowArbitrarySubtypes = true)) - .renderedTo("application/jsonpath, application/sql; charset=UTF-8") - // q-values are not meaningful for Accept-Query (RFC 10008) and must be stripped - "Accept-Query: application/json;q=0.8" =!= - `Accept-Query`(MediaTypes.`application/json`) - .renderedTo("application/json") - // q-values stripped even with other parameters preserved - "Accept-Query: application/json;charset=utf-8;q=0.5" =!= - `Accept-Query`(MediaTypes.`application/json`.withParams(Map("charset" -> "utf-8"))) - .renderedTo("application/json; charset=utf-8") + .renderedTo("application/jsonpath, application/sql;charset=UTF-8") + // Accept-Query parameters are Structured Field parameters, not Accept q-values + "Accept-Query: application/json;q=quality" =!= + `Accept-Query`(MediaTypes.`application/json`.withParams(Map("q" -> "quality"))) + .renderedTo("application/json;q=quality") + """Accept-Query: application/json;q="0.5"""" =!= + `Accept-Query`(MediaTypes.`application/json`.withParams(Map("q" -> "0.5"))) + .renderedTo("""application/json;q="0.5"""") + val headerWithQValue = `Accept-Query`(MediaTypes.`application/json`.withQValue(0.8)).unsafeToString + headerWithQValue shouldEqual "Accept-Query: application/json" + val headerWithQParam = `Accept-Query`(MediaTypes.`application/json`.withParams(Map("charset" -> "utf-8", + "q" -> "0.5"))).unsafeToString + headerWithQParam shouldEqual """Accept-Query: application/json;charset=utf-8;q="0.5"""" + val HttpHeader.ParsingResult.Ok(wildcardWithQParam, Nil) = + HttpHeader.parse("Accept-Query", """application/*;q="0.5"""") + wildcardWithQParam.unsafeToString shouldEqual """Accept-Query: application/*;q="0.5"""" + HttpHeader.parse("Accept-Query", "*").errors should not be empty + HttpHeader.parse("Accept-Query", "*/json").errors should not be empty + HttpHeader.parse("Accept-Query", "3/example").errors should not be empty + HttpHeader.parse("Accept-Query", "application/json;q=0.8").errors should not be empty + HttpHeader.parse("Accept-Query", "application/json;Charset=UTF-8").errors should not be empty + HttpHeader.parse("Accept-Query", """"application/json/extra"""").errors should not be empty } "Accept-Encoding" in { From e6f426de7291f54a4bac3fe762d344ab7ff2415b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Fri, 19 Jun 2026 16:38:48 +0800 Subject: [PATCH 4/5] fix: use standard ASF license header for AcceptQuery.java Motivation: AcceptQuery.java is a new file with original code, not derived from Akka. It should use the standard ASF license header, not the "derived from Akka" header. Modification: Replace apacheFromAkkaSourceHeader with the standard apacheHeader as defined in CopyrightHeader.scala. Result: AcceptQuery.java carries the correct license header for new contributions. References: Refs apache/pekko-http#1077 --- .../http/javadsl/model/headers/AcceptQuery.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java index 18c3b8bf49..c226c5ff69 100644 --- a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java @@ -1,10 +1,18 @@ /* * Licensed to the Apache Software Foundation (ASF) under one or more - * license agreements; and to You under the Apache License, version 2.0: + * 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 * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * This file is part of the Apache Pekko project, which was derived from Akka. + * 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.javadsl.model.headers; From ebd9990a8837dd1642b0cf26d42a08149903a8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Fri, 19 Jun 2026 16:50:45 +0800 Subject: [PATCH 5/5] refactor: replace private[this] with private in parser and headers Motivation: `private[this]` is being phased out in Scala 3 and provides no measurable benefit for vals in objects/traits. Modification: Replace `private[this]` with `private` in AcceptHeader.scala (3 vals) and headers.scala (1 val). Result: Cleaner code compatible with Scala 3 conventions. scalafmt applied. Tests: Not run - visibility-only change References: Refs apache/pekko-http#1077 --- .../apache/pekko/http/impl/model/parser/AcceptHeader.scala | 6 +++--- .../apache/pekko/http/scaladsl/model/headers/headers.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala index e859693080..6758f7e448 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/AcceptHeader.scala @@ -24,9 +24,9 @@ import pekko.http.impl.util._ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonActions => import CharacterClasses._ - private[this] val acceptQueryMediaRangeChar = tchar ++ '/' - private[this] val sfStringChar = CharPredicate('\u0020' to '\u0021', '\u0023' to '\u005B', '\u005D' to '\u007E') - private[this] val sfStringEscapedChar = CharPredicate('"', '\\') + private val acceptQueryMediaRangeChar = tchar ++ '/' + private val sfStringChar = CharPredicate('\u0020' to '\u0021', '\u0023' to '\u005B', '\u005D' to '\u007E') + private val sfStringEscapedChar = CharPredicate('"', '\\') // http://tools.ietf.org/html/rfc7231#section-5.3.2 def accept = rule { diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala index 3f13483cc4..852873a04f 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/headers/headers.scala @@ -239,7 +239,7 @@ object `Accept-Query` extends ModeledCompanion[`Accept-Query`] { def apply(firstMediaRange: MediaRange, otherMediaRanges: MediaRange*): `Accept-Query` = apply(firstMediaRange +: otherMediaRanges) - private[this] val sfTokenChar: CharPredicate = CharacterClasses.tchar ++ ":/" + private val sfTokenChar: CharPredicate = CharacterClasses.tchar ++ ":/" private implicit val mediaRangeRenderer: Renderer[MediaRange] = new Renderer[MediaRange] { def render[R <: Rendering](r: R, mediaRange: MediaRange): r.type = {