This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/main by this push:
new 4ac4b7355 feat: add RFC 10008 QUERY method and Accept-Query header
support (#1077)
4ac4b7355 is described below
commit 4ac4b7355d78d9815a3f91e775ae7a683cd9ede1
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Wed Jul 1 11:46:44 2026 +0800
feat: add RFC 10008 QUERY method and Accept-Query header support (#1077)
* feat: add RFC 10008 QUERY method and Accept-Query header support
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
* test: add RFC 10008 QUERY method property tests and clean up imports
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
* 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
* 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
* 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
---
.../routing-dsl/directives/alphabetically.md | 1 +
.../directives/method-directives/index.md | 1 +
.../directives/method-directives/query.md | 31 ++++++++
.../directives/MethodDirectivesExamplesTest.java | 16 ++++
.../directives/MethodDirectivesExamplesSpec.scala | 11 +++
.../pekko/http/javadsl/model/HttpMethods.java | 1 +
.../pekko/http/javadsl/model/HttpRequest.java | 5 ++
.../http/javadsl/model/headers/AcceptQuery.java | 35 +++++++++
.../http/impl/model/parser/AcceptHeader.scala | 89 +++++++++++++++++++++-
.../http/impl/model/parser/HeaderParser.scala | 1 +
.../pekko/http/scaladsl/model/HttpMethod.scala | 1 +
.../pekko/http/scaladsl/model/MediaRange.scala | 4 +
.../http/scaladsl/model/headers/headers.scala | 56 ++++++++++++++
.../http/impl/model/parser/HttpHeaderSpec.scala | 50 ++++++++++++
.../http/scaladsl/model/HttpMethodsSpec.scala | 15 ++++
.../http/scaladsl/model/headers/HeaderSpec.scala | 1 +
.../server/directives/MethodDirectivesSpec.scala | 13 +++-
.../server/directives/MethodDirectives.scala | 4 +
.../server/directives/MethodDirectives.scala | 8 ++
19 files changed, 340 insertions(+), 3 deletions(-)
diff --git a/docs/src/main/paradox/routing-dsl/directives/alphabetically.md
b/docs/src/main/paradox/routing-dsl/directives/alphabetically.md
index db9d59d20..7bcb5896b 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<List<Rejection>, RouteResult>`] function
|
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 3b12d24ec..a809bb4c1 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 000000000..839837ca6
--- /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 3c577f34f..5090d2f5a 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.complete;
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 class MethodDirectivesExamplesTest extends
JUnitJupiterRouteTest {
// #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 cb03e4f96..c2a256568 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 0db6d7485..808a6c1ad 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 @@ public final class 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 8d82d87c9..b5b3d0f22 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 abstract class HttpRequest
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 000000000..c226c5ff6
--- /dev/null
+++
b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/AcceptQuery.java
@@ -0,0 +1,35 @@
+/*
+ * 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.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-3
+ */
+public abstract class AcceptQuery extends
org.apache.pekko.http.scaladsl.model.HttpHeader {
+ public abstract Iterable<MediaRange> getMediaRanges();
+
+ public static AcceptQuery create(MediaRange... mediaRanges) {
+ return new org.apache.pekko.http.scaladsl.model.headers.Accept$minusQuery(
+ Util.<MediaRange,
org.apache.pekko.http.scaladsl.model.MediaRange>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 c38f79aa2..6758f7e44 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,19 +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 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 {
zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_))
}
+ // 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").
+ 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` ~ 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))
+ }
+ }
+ }
+
+ def `accept-query-media-range-def` = rule {
+ `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/impl/model/parser/HeaderParser.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/HeaderParser.scala
index e82c6aafc..870da7aa7 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 a00caae9b..d40798624 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/MediaRange.scala
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/MediaRange.scala
index c8d00fe67..af1d66a72 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
@@ -93,6 +93,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 e8c1c716d..852873a04 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,6 +234,60 @@ 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-3
+object `Accept-Query` extends ModeledCompanion[`Accept-Query`] {
+ def apply(firstMediaRange: MediaRange, otherMediaRanges: MediaRange*):
`Accept-Query` =
+ apply(firstMediaRange +: otherMediaRanges)
+
+ 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 = {
+ 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 {
+ 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 644d0f075..a7a8658be 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,56 @@ class HttpHeaderSpec extends AnyFreeSpec with Matchers {
"Accept-Ranges: none" =!= `Accept-Ranges`()
}
+ "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
+ "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)
+ val jsonPath = MediaType.customBinary("application", "jsonpath",
MediaType.Compressible,
+ allowArbitrarySubtypes = true)
+ """Accept-Query: "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`(
+ jsonPath,
+ MediaType.customBinary("application", "sql", MediaType.Compressible,
+ params = Map("charset" -> "UTF-8"), allowArbitrarySubtypes = true))
+ .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 {
"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 16a198e8d..f60dd55cb 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,20 @@ 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))
+ }
+ }
+
+ "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-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 73424fbb7..f5af11aff 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 3d3d72cae..e6ec218b3 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, 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 871d835fa..d5b69f9e3 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 3a18eda3c..de4e1e6ca 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
@@ -79,6 +79,13 @@ trait MethodDirectives {
*/
def put: Directive0 = _put
+ /**
+ * Rejects all non-QUERY requests.
+ *
+ * @group method
+ */
+ def query: Directive0 = _query
+
/**
* Extracts the request method.
*
@@ -133,5 +140,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
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]