diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
index e92582efd..22f27425c 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
@@ -65,6 +65,7 @@
import org.apache.hc.core5.http.protocol.RequestConnControl;
import org.apache.hc.core5.http.protocol.RequestContent;
import org.apache.hc.core5.http.protocol.RequestExpectContinue;
+import org.apache.hc.core5.http.protocol.RequestTE;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
import org.apache.hc.core5.http.protocol.RequestUserAgent;
import org.apache.hc.core5.testing.extension.classic.ClassicTestResources;
@@ -638,7 +639,8 @@ void testHttpPostNoContentLength() throws Exception {
RequestTargetHost.INSTANCE,
RequestConnControl.INSTANCE,
RequestUserAgent.INSTANCE,
- RequestExpectContinue.INSTANCE));
+ RequestExpectContinue.INSTANCE,
+ RequestTE.INSTANCE));
client.start();
final HttpCoreContext context = HttpCoreContext.create();
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java
index 3da9d11dd..db96adfdb 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java
@@ -32,6 +32,7 @@
import org.apache.hc.core5.http.protocol.RequestConnControl;
import org.apache.hc.core5.http.protocol.RequestContent;
import org.apache.hc.core5.http.protocol.RequestExpectContinue;
+import org.apache.hc.core5.http.protocol.RequestTE;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
import org.apache.hc.core5.http.protocol.RequestUserAgent;
import org.apache.hc.core5.http.protocol.RequestValidateHost;
@@ -112,6 +113,44 @@ public static HttpProcessorBuilder customClient(final String agentInfo) {
RequestExpectContinue.INSTANCE);
}
+ /**
+ * Creates an {@link HttpProcessorBuilder} initialized with strict protocol interceptors
+ * for client-side HTTP/1.1 processing.
+ *
+ * This configuration enforces stricter validation and processing of client requests,
+ * ensuring compliance with the HTTP protocol. It includes interceptors for handling
+ * target hosts, content, connection controls, and TE header validation, among others.
+ * The user agent can be customized using the provided {@code agentInfo} parameter.
+ *
+ * @param agentInfo the user agent info to be included in the {@code User-Agent} header.
+ * If {@code null} or blank, a default value will be used.
+ * @return the {@link HttpProcessorBuilder} configured with strict client-side interceptors.
+ * @since 5.4
+ */
+ public static HttpProcessorBuilder strictClient(final String agentInfo) {
+ return HttpProcessorBuilder.create()
+ .addAll(
+ RequestTargetHost.INSTANCE,
+ RequestContent.INSTANCE,
+ RequestConnControl.INSTANCE,
+ RequestTE.INSTANCE,
+ new RequestUserAgent(!TextUtils.isBlank(agentInfo) ? agentInfo :
+ VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", HttpProcessors.class)),
+ RequestExpectContinue.INSTANCE);
+ }
+
+ /**
+ * Creates {@link HttpProcessorBuilder} initialized with default protocol interceptors
+ * for client side HTTP/1.1 processing.
+ *
+ * @param agentInfo the agent info text or {@code null} for default.
+ * @return the processor builder.
+ * @since 5.4
+ */
+ public static HttpProcessorBuilder customClient(final String agentInfo, final boolean strict) {
+ return strict ? strictClient(agentInfo) : customClient(agentInfo);
+ }
+
/**
* Creates {@link HttpProcessor} initialized with default protocol interceptors
* for client side HTTP/1.1 processing.
@@ -120,7 +159,7 @@ public static HttpProcessorBuilder customClient(final String agentInfo) {
* @return the processor.
*/
public static HttpProcessor client(final String agentInfo) {
- return customClient(agentInfo).build();
+ return client(agentInfo, false);
}
/**
@@ -130,7 +169,47 @@ public static HttpProcessor client(final String agentInfo) {
* @return the processor.
*/
public static HttpProcessor client() {
- return customClient(null).build();
+ return client(null);
+ }
+
+ /**
+ * Creates an {@link HttpProcessor} for client-side HTTP/2 processing.
+ * This method allows the option to include strict protocol interceptors.
+ *
+ * @param agentInfo the agent info text or {@code null} for default.
+ * @param strict if {@code true}, strict protocol interceptors will be added, including the {@code TE} header validation.
+ * @return the configured HTTP processor.
+ * @since 5.4
+ */
+ public static HttpProcessor client(final String agentInfo, final boolean strict) {
+ return customClient(agentInfo, strict).build();
+ }
+
+ /**
+ * Creates an {@link HttpProcessor} for client-side HTTP/2 processing
+ * with strict protocol validation interceptors by default.
+ *
+ * Strict validation includes additional checks such as validating the {@code TE} header.
+ *
+ * @return the configured strict HTTP processor.
+ * @since 5.4
+ */
+ public static HttpProcessor clientStrict() {
+ return customClient(null, true).build();
+ }
+
+ /**
+ * Creates an {@link HttpProcessor} for client-side HTTP/2 processing
+ * with strict protocol validation interceptors, using the specified agent information.
+ *
+ * Strict validation includes additional checks such as validating the {@code TE} header.
+ *
+ * @param agentInfo the agent info text or {@code null} for default.
+ * @return the configured strict HTTP processor.
+ * @since 5.4
+ */
+ public static HttpProcessor clientStrict(final String agentInfo) {
+ return customClient(agentInfo, true).build();
}
}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java
new file mode 100644
index 000000000..dcf6cd719
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java
@@ -0,0 +1,120 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.core5.http.protocol;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.message.MessageSupport;
+import org.apache.hc.core5.util.Args;
+
+
+/**
+ * HTTP protocol interceptor responsible for validating and processing the {@link HttpHeaders#TE} header field in HTTP/1.1 requests.
+ *
+ * The {@link HttpHeaders#TE} header is used to indicate transfer codings the client is willing to accept and, in some cases, whether
+ * the client is willing to accept trailer fields. This interceptor ensures that the {@link HttpHeaders#TE} header does not include
+ * the {@code chunked} transfer coding and validates the presence of the {@code Connection: TE} header.
+ *
+ * For HTTP/1.1 requests, the {@link HttpHeaders#TE} header can contain multiple values separated by commas and may include quality
+ * values (denoted by {@code q=}) separated by semicolons.
+ *
+ * In case of HTTP/2, this validation is skipped, and another layer of logic handles the specifics of HTTP/2 compliance.
+ *
+ * @since 5.4
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class RequestTE implements HttpRequestInterceptor {
+
+ /**
+ * Singleton instance of the {@code RequestTE} interceptor.
+ */
+ public static final HttpRequestInterceptor INSTANCE = new RequestTE();
+
+ /**
+ * Default constructor.
+ */
+ public RequestTE() {
+ super();
+ }
+
+ /**
+ * Processes the {@code TE} header of the given HTTP request and ensures compliance with HTTP/1.1 requirements.
+ *
+ * If the {@code TE} header is present, this method validates that:
+ *
+ * - The {@code TE} header does not include the {@code chunked} transfer coding, which is implicitly supported for HTTP/1.1.
+ * - The {@code Connection} header includes the {@code TE} directive, as required by the protocol.
+ *
+ *
+ * @param request the HTTP request containing the headers to validate
+ * @param entity the entity associated with the request (may be {@code null})
+ * @param context the execution context for the request
+ * @throws HttpException if the {@code TE} header contains invalid values or the {@code Connection} header is missing
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ final AtomicBoolean hasTE = new AtomicBoolean(false);
+ final AtomicBoolean hasChunk = new AtomicBoolean(false);
+ MessageSupport.parseTokens(request, HttpHeaders.TE, token -> {
+ hasTE.set(true);
+ if (token.equalsIgnoreCase("chunked")) {
+ hasChunk.set(true);
+ }
+ });
+ if (hasChunk.get()) {
+ throw new ProtocolException("'chunked' transfer coding must not be listed in the TE header for HTTP/1.1.");
+ }
+ if (hasTE.get()) {
+ final AtomicBoolean hasConnection = new AtomicBoolean(false);
+ final AtomicBoolean hasTEinConnection = new AtomicBoolean(false);
+ MessageSupport.parseTokens(request, HttpHeaders.CONNECTION, token -> {
+ hasConnection.set(true);
+ if ("TE".equalsIgnoreCase(token)) {
+ hasTEinConnection.set(true);
+ }
+ });
+ if (!hasTEinConnection.get()) {
+ throw new ProtocolException("The 'Connection' header must include the 'TE' directive when the 'TE' header is present.");
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java
new file mode 100644
index 000000000..990c4909b
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java
@@ -0,0 +1,199 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.core5.http.protocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestRequestTE {
+
+ @Test
+ void testValidTEHeader() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ // Set the TE header and Connection header
+ request.setHeader(HttpHeaders.TE, "trailers");
+ request.setHeader(HttpHeaders.CONNECTION, "TE");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ assertNotNull(request.getHeader(HttpHeaders.TE));
+ assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue());
+ }
+
+
+ @Test
+ void testMultipleValidTEHeaders() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ // Set both the TE header and the Connection header
+ request.setHeader(HttpHeaders.TE, "trailers, deflate;q=0.5");
+ request.setHeader(HttpHeaders.CONNECTION, "TE");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ assertNotNull(request.getHeader(HttpHeaders.TE));
+ assertEquals("trailers, deflate;q=0.5", request.getHeader(HttpHeaders.TE).getValue());
+ }
+
+
+ @Test
+ void testTEHeaderNotPresent() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ // No TE header, no validation should occur
+ assertNull(request.getHeader(HttpHeaders.TE));
+ }
+
+ @Test
+ void testTEHeaderContainsChunked() {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+ request.setHeader(HttpHeaders.TE, "chunked");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ interceptor.process(request, request.getEntity(), context));
+ }
+
+ @Test
+ void testTEHeaderInvalidTransferCoding() {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+ request.setHeader(HttpHeaders.TE, "invalid;q=abc");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ interceptor.process(request, request.getEntity(), context));
+ }
+
+ @Test
+ void testTEHeaderAlreadySet() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ final String teValue = "trailers";
+ request.setHeader(HttpHeaders.TE, teValue);
+ request.setHeader(HttpHeaders.CONNECTION, "TE"); // Add the Connection header as required
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName());
+ assertNotNull(request.getHeader(HttpHeaders.TE));
+ assertEquals(teValue, request.getHeader(HttpHeaders.TE).getValue());
+ }
+
+
+ @Test
+ void testTEHeaderWithConnectionHeaderValidation() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+ request.setHeader(HttpHeaders.TE, "trailers");
+ request.setHeader(HttpHeaders.CONNECTION, "TE");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName());
+ assertNotNull(request.getHeader(HttpHeaders.TE));
+ assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue());
+ }
+
+ @Test
+ void testTEHeaderWithoutConnectionHeaderThrowsException() {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+ request.setHeader(HttpHeaders.TE, "trailers");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ interceptor.process(request, request.getEntity(), context));
+ }
+
+ @Test
+ void testTEHeaderWithoutTEInConnectionHeaderThrowsException() {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+ // Set TE header but Connection header does not include "TE"
+ request.setHeader(HttpHeaders.TE, "trailers");
+ request.setHeader(HttpHeaders.CONNECTION, "keep-alive"); // Missing "TE"
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ interceptor.process(request, request.getEntity(), context));
+ }
+
+ @Test
+ void testTEHeaderWithMultipleDirectivesInConnectionHeader() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ // Set TE header and a Connection header with multiple directives
+ request.setHeader(HttpHeaders.TE, "trailers");
+ request.setHeader(HttpHeaders.CONNECTION, "keep-alive, close, TE");
+
+ final HttpRequestInterceptor interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ assertNotNull(request.getHeader(HttpHeaders.CONNECTION));
+ assertTrue(request.getHeader(HttpHeaders.CONNECTION).getValue().contains("TE"));
+ }
+
+
+}
+
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
index bdf336a0d..e9f66b744 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
@@ -442,6 +442,25 @@ void testRequestTargetHostConnectHttp10() throws Exception {
Assertions.assertNull(header);
}
+ @Test
+ void testTEHeaderWithConnectionTE() throws Exception {
+ final HttpCoreContext context = HttpCoreContext.create();
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ // Set both TE and Connection headers as per the requirement
+ request.setHeader(HttpHeaders.TE, "trailers");
+ request.setHeader(HttpHeaders.CONNECTION, "TE");
+
+ final RequestTE interceptor = new RequestTE();
+ interceptor.process(request, request.getEntity(), context);
+
+ final Header connectionHeader = request.getFirstHeader(HttpHeaders.CONNECTION);
+ Assertions.assertNotNull(connectionHeader);
+ Assertions.assertEquals("TE", connectionHeader.getValue());
+ }
+
+
@Test
void testRequestUserAgentGenerated() throws Exception {
final HttpCoreContext context = HttpCoreContext.create();