You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pekko.apache.org by fa...@apache.org on 2025/06/17 14:53:26 UTC
(pekko-http) branch main updated: Add trailer header (#684)
This is an automated email from the ASF dual-hosted git repository.
fanningpj 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 f910254ac Add trailer header (#684)
f910254ac is described below
commit f910254ac52f3cee653e9b33d35bbd298b53fc0f
Author: navid <na...@moia.io>
AuthorDate: Tue Jun 17 14:53:21 2025 +0000
Add trailer header (#684)
* add trailer header
* add docs + format
* add Trailer to list of @ApiMayChange
* run javafmtAll
* Seq -> immutable.Seq
* fix incomplete javadoc
* disallow creation of trailer header with forbidden values + improve parsing error messages
* use an ExceptionWithErrorInfo
* add ApiMayChange to object as well
---
docs/src/main/paradox/compatibility-guidelines.md | 1 +
.../pekko/http/javadsl/model/headers/Trailer.java | 44 +++++++++++++
.../http/impl/model/parser/HeaderParser.scala | 3 +-
.../http/impl/model/parser/SimpleHeaders.scala | 3 +
.../http/scaladsl/model/headers/headers.scala | 72 +++++++++++++++++++++-
.../http/impl/model/parser/HttpHeaderSpec.scala | 23 ++++++-
.../http/scaladsl/model/headers/HeaderSpec.scala | 12 ++--
.../engine/http2/HttpMessageRenderingSpec.scala | 2 +-
8 files changed, 152 insertions(+), 8 deletions(-)
diff --git a/docs/src/main/paradox/compatibility-guidelines.md b/docs/src/main/paradox/compatibility-guidelines.md
index c73965384..107cfbebc 100644
--- a/docs/src/main/paradox/compatibility-guidelines.md
+++ b/docs/src/main/paradox/compatibility-guidelines.md
@@ -86,6 +86,7 @@ Scala
org.apache.pekko.http.scaladsl.model.headers.CacheDirectives.immutableDirective
org.apache.pekko.http.scaladsl.model.headers.X-Forwarded-Host
org.apache.pekko.http.scaladsl.model.headers.X-Forwarded-Proto
+ org.apache.pekko.http.scaladsl.model.headers.Trailer
org.apache.pekko.http.scaladsl.model.http2.PeerClosedStreamException
org.apache.pekko.http.scaladsl.model.http2.Http2Exception
org.apache.pekko.http.scaladsl.model.SimpleRequestResponseAttribute
diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/Trailer.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/Trailer.java
new file mode 100644
index 000000000..27670498b
--- /dev/null
+++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/headers/Trailer.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * The `Trailer` header is used before a message body to indicate which fields will be present in
+ * the trailers when using chunked transfer encoding. See <a
+ * href="https://datatracker.ietf.org/doc/html/rfc7230#section-4.4">RFC 7230, Section 4.4</a>
+ *
+ * @since 1.3.0
+ */
+public abstract class Trailer extends org.apache.pekko.http.scaladsl.model.HttpHeader {
+ /**
+ * @return an iterable collection of field names that are expected to be present in the trailer
+ */
+ public abstract Iterable<String> getTrailers();
+
+ /**
+ * Creates a new `Trailer` header with the specified trailer field names.
+ *
+ * @param values the names of the fields that are expected to be present in the trailer
+ * @return a new `Trailer` header instance
+ */
+ public static Trailer create(String... values) {
+ return org.apache.pekko.http.scaladsl.model.headers.Trailer.apply(Util.convertArray(values));
+ }
+}
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 68ae18e63..b770723d6 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
@@ -201,7 +201,8 @@ private[http] object HeaderParser {
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
- "x-real-ip")
+ "x-real-ip",
+ "trailer")
abstract class Settings {
def uriParsingMode: Uri.ParsingMode
diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/SimpleHeaders.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/SimpleHeaders.scala
index 306db5e35..0522a774b 100644
--- a/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/SimpleHeaders.scala
+++ b/http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/SimpleHeaders.scala
@@ -284,4 +284,7 @@ private[parser] trait SimpleHeaders {
(`ip-v4-address` | `ip-v6-address`) ~ EOI ~> (b => `X-Real-Ip`(RemoteAddress(b)))
}
+ def trailer = rule {
+ zeroOrMore(token).separatedBy(listSep) ~ EOI ~> (Trailer(_))
+ }
}
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 05d9cb565..e6aa0e7d1 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
@@ -17,7 +17,6 @@ import java.lang.Iterable
import java.net.InetSocketAddress
import java.security.MessageDigest
import java.util
-
import javax.net.ssl.SSLSession
import org.apache.pekko
import pekko.annotation.{ ApiMayChange, InternalApi }
@@ -1226,3 +1225,74 @@ final case class `X-Real-Ip`(address: RemoteAddress) extends jm.headers.XRealIp
def renderValue[R <: Rendering](r: R): r.type = r ~~ address
protected def companion = `X-Real-Ip`
}
+
+@ApiMayChange
+object Trailer extends ModeledCompanion[Trailer] {
+ private implicit val trailersRenderer: Renderer[immutable.Iterable[String]] =
+ Renderer.defaultSeqRenderer[String]
+
+ def apply(values: immutable.Seq[String]): Trailer = {
+ val clean = values.map(_.trim).filter(_.nonEmpty)
+ val (forbidden, allowed) = clean.partition(name => isForbidden(name.toRootLowerCase))
+ if (clean.isEmpty)
+ throw IllegalHeaderException(
+ "Trailer values must not be empty",
+ "No valid header names specified")
+ else if (forbidden.nonEmpty) {
+ val forbiddenInput = forbidden.mkString("[", ", ", "]")
+ throw IllegalHeaderException(
+ "Trailer values must not contain forbidden header names",
+ s"Trailer contained $forbiddenInput")
+ } else new Trailer(allowed)
+ }
+
+ /**
+ * Non-exhaustive set of header names that are disallowed in the `Trailer` header.
+ * See <a href="https://datatracker.ietf.org/doc/html/rfc7230#section-4.1.2">RFC 7230, Section 4.1.2</a>
+ */
+ private val isForbidden: Set[String] =
+ Set(
+ Trailer,
+ // Framing
+ `Transfer-Encoding`,
+ `Content-Length`,
+ // Routing
+ Host,
+ // Modifiers
+ Expect,
+ Range,
+ `If-Match`,
+ `If-None-Match`,
+ `If-Modified-Since`,
+ `If-Unmodified-Since`,
+ `Cache-Control`,
+ TE,
+ // Auth and cookies
+ Authorization,
+ `Proxy-Authorization`,
+ `Cookie`,
+ `Set-Cookie`,
+ // Response control
+ `WWW-Authenticate`,
+ Age,
+ // Payload processing
+ `Content-Encoding`,
+ `Content-Type`,
+ `Content-Range`).map(_.lowercaseName)
+}
+
+/**
+ * The `Trailer` header is used before a message body to indicate which fields will be present
+ * in the trailers when using chunked transfer encoding.
+ * See <a href="https://datatracker.ietf.org/doc/html/rfc7230#section-4.4">RFC 7230, Section 4.4</a>
+ *
+ * @since 1.3.0
+ */
+@ApiMayChange
+final case class Trailer private (values: immutable.Seq[String]) extends jm.headers.Trailer with RequestResponseHeader {
+ require(values.nonEmpty, "Trailer values must not be empty")
+ import Trailer.trailersRenderer
+ def getTrailers: Iterable[String] = values.asJava
+ protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ values
+ protected def companion: ModeledCompanion[Trailer] = Trailer
+}
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 9314d51ab..b8ef0594e 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
@@ -19,7 +19,7 @@ import pekko.http.impl.model.parser.HeaderParser.Settings
import org.scalatest.matchers.{ MatchResult, Matcher }
import pekko.http.impl.util._
import pekko.http.scaladsl.model.{ HttpHeader, _ }
-import headers._
+import headers.{ Trailer => `Trailer`, _ }
import CacheDirectives._
import MediaTypes._
import MediaRanges._
@@ -28,6 +28,7 @@ import HttpEncodings._
import HttpMethods._
import java.net.InetAddress
+import scala.collection.immutable.Seq
import pekko.http.scaladsl.model.MediaType.WithOpenCharset
import org.scalatest.exceptions.TestFailedException
import org.scalatest.freespec.AnyFreeSpec
@@ -820,6 +821,26 @@ class HttpHeaderSpec extends AnyFreeSpec with Matchers {
"pekko.apache.org\n^")
}
+ "Trailer" in {
+ "Trailer: one" =!= `Trailer`(Seq("one"))
+ "Trailer: one, two" =!= `Trailer`(Seq("one", "two"))
+ "Trailer: ignore,spacing, between, values" =!=> "ignore, spacing, between, values"
+ "Trailer: Content-MD5 , ETag, X-Custom-Field" =!= `Trailer`(Seq("Content-MD5", "ETag", "X-Custom-Field"))
+ .renderedTo("Content-MD5, ETag, X-Custom-Field")
+ "Trailer: content-md5, ETAG, X-Custom-Field" =!= `Trailer`(Seq("content-md5", "ETAG", "X-Custom-Field"))
+ "Trailer: " =!= ErrorInfo(
+ summary = "Illegal HTTP header 'Trailer': Trailer values must not be empty",
+ detail = "No valid header names specified")
+ "Trailer: X-Custom-Hash, X-Custom-Signature" =!= `Trailer`(Seq("X-Custom-Hash", "X-Custom-Signature"))
+ "Trailer: Content@Md5" =!= ErrorInfo(
+ summary =
+ "Illegal HTTP header 'Trailer': Invalid input '@', expected tchar, OWS, listSep or 'EOI' (line 1, column 8)",
+ detail = "Content@Md5\n ^")
+ "Trailer: Content-Length" =!= ErrorInfo(
+ summary = "Illegal HTTP header 'Trailer': Trailer values must not contain forbidden header names",
+ detail = "Trailer contained [Content-Length]")
+ }
+
"RawHeader" in {
"X-Space-Ranger: no, this rock!" =!= RawHeader("X-Space-Ranger", "no, this rock!")
}
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 a2317218d..663b80fb1 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
@@ -15,12 +15,14 @@ package org.apache.pekko.http.scaladsl.model.headers
import org.apache.pekko
import pekko.http.impl.util._
-import java.net.InetAddress
-import pekko.http.scaladsl.model.{ headers, _ }
+import java.net.InetAddress
+import pekko.http.scaladsl.model.{ headers, Trailer => _, _ }
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
+import scala.collection.immutable
+
class HeaderSpec extends AnyFreeSpec with Matchers {
"ModeledCompanion should" - {
"provide parseFromValueString method" - {
@@ -184,7 +186,8 @@ class HeaderSpec extends AnyFreeSpec with Matchers {
`X-Forwarded-For`(RemoteAddress(InetAddress.getByName("192.168.0.1"))),
`X-Forwarded-Host`(Uri.Host(InetAddress.getByName("192.168.0.2"))),
`X-Forwarded-Proto`("https"),
- `X-Real-Ip`(RemoteAddress(InetAddress.getByName("192.168.1.1"))))
+ `X-Real-Ip`(RemoteAddress(InetAddress.getByName("192.168.1.1"))),
+ Trailer(immutable.Seq("X-My-Trailer", "X-My-Other-Trailer")))
requestHeaders.foreach { header =>
header shouldBe Symbol("renderInRequests")
@@ -227,7 +230,8 @@ class HeaderSpec extends AnyFreeSpec with Matchers {
`Transfer-Encoding`(TransferEncodings.chunked),
Upgrade(Vector(UpgradeProtocol("HTTP", Some("2.0")))),
`WWW-Authenticate`(HttpChallenge("Basic", Some("example.com"))),
- `Retry-After`(120))
+ `Retry-After`(120),
+ Trailer(immutable.Seq("X-My-Trailer", "X-My-Other-Trailer")))
responseHeaders.foreach { header =>
header shouldBe Symbol("renderInResponses")
diff --git a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRenderingSpec.scala b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRenderingSpec.scala
index 09fc4aca7..7c7a53f6d 100644
--- a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRenderingSpec.scala
+++ b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRenderingSpec.scala
@@ -19,7 +19,7 @@ import com.typesafe.config.ConfigFactory
import org.apache.pekko
import pekko.event.NoLogging
import pekko.http.impl.engine.rendering.DateHeaderRendering
-import pekko.http.scaladsl.model.headers._
+import pekko.http.scaladsl.model.headers.{ Trailer => _, _ }
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.settings.{ ClientConnectionSettings, ServerSettings }
import org.scalatest.matchers.should.Matchers
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pekko.apache.org
For additional commands, e-mail: commits-help@pekko.apache.org