From 232773f5494454126dcd5f99e7e9ba1378c8ec3f Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Thu, 16 Oct 2025 10:37:29 +0200 Subject: [PATCH] add experimental RFC 9218 Priority header support. Wire H2RequestPriority via H2AsyncClientBuilder.enablePriorityHeader(); omit on defaults, allow overwrite. --- .../hc/client5/http/config/RequestConfig.java | 39 ++++- .../http/impl/async/H2AsyncClientBuilder.java | 20 +++ .../impl/async/HttpAsyncClientBuilder.java | 19 +++ .../http/protocol/H2RequestPriority.java | 98 +++++++++++ .../http/examples/AsyncClientH2Priority.java | 113 +++++++++++++ .../http/protocol/H2RequestPriorityTest.java | 152 ++++++++++++++++++ 6 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/protocol/H2RequestPriority.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Priority.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/protocol/H2RequestPriorityTest.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java index 8b39e0ee1b..1de83db938 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java @@ -32,8 +32,10 @@ import java.util.concurrent.TimeUnit; import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http2.priority.PriorityValue; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; @@ -69,13 +71,18 @@ public class RequestConfig implements Cloneable { private final ExpectContinueTrigger expectContinueTrigger; + /** + * HTTP/2 Priority header value to emit when using H2+. Null means “don’t emit”. + */ + private final PriorityValue h2Priority; + /** * Intended for CDI compatibility */ protected RequestConfig() { this(false, null, null, false, false, 0, false, null, null, DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null, - ExpectContinueTrigger.ALWAYS); + ExpectContinueTrigger.ALWAYS, null); } RequestConfig( @@ -96,7 +103,8 @@ protected RequestConfig() { final boolean hardCancellationEnabled, final boolean protocolUpgradeEnabled, final Path unixDomainSocket, - final ExpectContinueTrigger expectContinueTrigger) { + final ExpectContinueTrigger expectContinueTrigger, + final PriorityValue h2Priority) { super(); this.expectContinueEnabled = expectContinueEnabled; this.proxy = proxy; @@ -116,6 +124,7 @@ protected RequestConfig() { this.protocolUpgradeEnabled = protocolUpgradeEnabled; this.unixDomainSocket = unixDomainSocket; this.expectContinueTrigger = expectContinueTrigger; + this.h2Priority = h2Priority; } /** @@ -244,6 +253,15 @@ public ExpectContinueTrigger getExpectContinueTrigger() { return expectContinueTrigger; } + /** + * Returns the HTTP/2+ priority preference for this request or {@code null} if unset. + * @since 5.6 + */ + @Experimental + public PriorityValue getH2Priority() { + return h2Priority; + } + @Override protected RequestConfig clone() throws CloneNotSupportedException { return (RequestConfig) super.clone(); @@ -296,7 +314,8 @@ public static RequestConfig.Builder copy(final RequestConfig config) { .setContentCompressionEnabled(config.isContentCompressionEnabled()) .setHardCancellationEnabled(config.isHardCancellationEnabled()) .setProtocolUpgradeEnabled(config.isProtocolUpgradeEnabled()) - .setUnixDomainSocket(config.getUnixDomainSocket()); + .setUnixDomainSocket(config.getUnixDomainSocket()) + .setH2Priority(config.getH2Priority()); } public static class Builder { @@ -319,6 +338,7 @@ public static class Builder { private boolean protocolUpgradeEnabled; private Path unixDomainSocket; private ExpectContinueTrigger expectContinueTrigger; + private PriorityValue h2Priority; Builder() { super(); @@ -691,6 +711,16 @@ public Builder setExpectContinueTrigger(final ExpectContinueTrigger trigger) { return this; } + /** + * Sets HTTP/2+ request priority. If {@code null}, the header is not emitted. + * @since 5.6 + */ + @Experimental + public Builder setH2Priority(final PriorityValue priority) { + this.h2Priority = priority; + return this; + } + public RequestConfig build() { return new RequestConfig( expectContinueEnabled, @@ -710,7 +740,8 @@ public RequestConfig build() { hardCancellationEnabled, protocolUpgradeEnabled, unixDomainSocket, - expectContinueTrigger); + expectContinueTrigger, + h2Priority); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java index c171e695a4..8e6b02dd92 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java @@ -64,6 +64,7 @@ import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider; import org.apache.hc.client5.http.impl.nio.MultihomeConnectionInitiator; import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; +import org.apache.hc.client5.http.protocol.H2RequestPriority; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; @@ -71,6 +72,7 @@ import org.apache.hc.client5.http.protocol.ResponseProcessCookies; import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.DefaultThreadFactory; import org.apache.hc.core5.function.Callback; @@ -216,6 +218,8 @@ private ExecInterceptorEntry( private Decorator ioSessionDecorator; + private boolean priorityHeaderDisabled; + public static H2AsyncClientBuilder create() { return new H2AsyncClientBuilder(); } @@ -312,6 +316,16 @@ public final H2AsyncClientBuilder setIoSessionDecorator(final Decorator @@ -762,6 +777,11 @@ public CloseableHttpAsyncClient build() { } } } + + if (!priorityHeaderDisabled) { + b.addLast(H2RequestPriority.INSTANCE); + } + b.addAll( new H2RequestTargetHost(), new RequestDefaultHeaders(defaultHeaders), diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java index 26f7022209..b2f55bba79 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java @@ -78,6 +78,7 @@ import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.protocol.H2RequestPriority; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; @@ -87,6 +88,7 @@ import org.apache.hc.client5.http.protocol.RequestValidateTrace; import org.apache.hc.client5.http.protocol.ResponseProcessCookies; import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.DefaultThreadFactory; import org.apache.hc.core5.function.Callback; @@ -269,6 +271,9 @@ private ExecInterceptorEntry( private EarlyHintsListener earlyHintsListener; + private boolean priorityHeaderDisabled; + + /** * Maps {@code Content-Encoding} tokens to decoder factories in insertion order. */ @@ -896,6 +901,16 @@ public HttpAsyncClientBuilder disableContentCompression() { return this; } + /** + * Disable installing the HTTP/2 Priority header interceptor by default. + * @since 5.6 + */ + @Experimental + public final HttpAsyncClientBuilder disableRequestPriority() { + this.priorityHeaderDisabled = true; + return this; + } + /** * Registers a global {@link org.apache.hc.client5.http.EarlyHintsListener} * that will be notified when the client receives {@code 103 Early Hints} @@ -1039,6 +1054,10 @@ public CloseableHttpAsyncClient build() { } } + if (!priorityHeaderDisabled) { + b.addLast(H2RequestPriority.INSTANCE); + } + final HttpProcessor httpProcessor = b.build(); final NamedElementChain execChainDefinition = new NamedElementChain<>(); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/H2RequestPriority.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/H2RequestPriority.java new file mode 100644 index 0000000000..a150a370ca --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/H2RequestPriority.java @@ -0,0 +1,98 @@ +/* + * ==================================================================== + * 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.client5.http.protocol; + +import java.io.IOException; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +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.HttpVersion; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http2.priority.PriorityFormatter; +import org.apache.hc.core5.http2.priority.PriorityValue; +import org.apache.hc.core5.util.Args; + +/** + * Adds the {@code Priority} request header to HTTP/2+ requests when a per-request + * priority is configured. + *

+ * The priority is taken from {@link RequestConfig#getH2Priority()}. If a {@code Priority} + * header is already present on the request, it is left unchanged. If formatting the + * configured value yields an empty string (e.g., because it encodes protocol defaults), + * the header is not added. + * + * @since 5.6 + */ +@Experimental +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public final class H2RequestPriority implements HttpRequestInterceptor { + + /** + * Singleton instance. + */ + public static final H2RequestPriority INSTANCE = new H2RequestPriority(); + + @Override + public void process( + final HttpRequest request, + final EntityDetails entity, + final HttpContext context) throws HttpException, IOException { + + Args.notNull(request, "HTTP request"); + Args.notNull(context, "HTTP context"); + + final HttpClientContext httpClientContext = HttpClientContext.cast(context); + + final ProtocolVersion pv = httpClientContext.getProtocolVersion(); + if (pv.compareToVersion(HttpVersion.HTTP_2) < 0) { + return; // only for HTTP/2+ + } + + final Header existing = request.getFirstHeader(HttpHeaders.PRIORITY); + if (existing != null) { + return; + } + + final RequestConfig requestConfig = httpClientContext.getRequestConfigOrDefault(); + final PriorityValue pri = requestConfig.getH2Priority(); + if (pri == null || PriorityValue.defaults().equals(pri)) { + return; + } + + request.addHeader(PriorityFormatter.formatHeader(pri)); + } +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Priority.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Priority.java new file mode 100644 index 0000000000..863fc46344 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Priority.java @@ -0,0 +1,113 @@ +/* + * ==================================================================== + * 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.client5.http.examples; + +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.priority.PriorityValue; + +/** + * Demonstrates sending the HTTP/2 {@code Priority} request header using per-request configuration. + * + *

How it works: + *

    + *
  • Call {@code enablePriorityHeader()} on the H2 client builder to register + * {@link org.apache.hc.client5.http.protocol.H2RequestPriority}.
  • + *
  • For each request, set a priority on the {@link RequestConfig} via + * {@link RequestConfig.Builder#setH2Priority(PriorityValue)} and attach it to the + * {@link HttpClientContext} passed to {@code execute}.
  • + *
+ * + *

Notes: + *

    + *
  • If a {@code Priority} header is already present on the request, it is preserved.
  • + *
  • If the configured value encodes protocol defaults, the header is omitted.
  • + *
  • Applies to HTTP/2+ only; HTTP/1.1 requests are unaffected.
  • + *
+ * + * @since 5.6 + */ +@Experimental +public class AsyncClientH2Priority { + + public static void main(final String[] args) throws Exception { + try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create() + .setH2Config(H2Config.custom() + .setPushEnabled(false) + .build()) + .build()) { + + client.start(); + + // --- Request 1: non-default priority -> header sent (e.g., "u=0, i") + final HttpClientContext ctx1 = HttpClientContext.create(); + ctx1.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(0, true)) + .build()); + + final SimpleHttpRequest req1 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/headers").build(); + final Future f1 = client.execute(req1, ctx1, null); + final SimpleHttpResponse r1 = f1.get(); + System.out.println("[/httpbin/headers] -> " + r1.getCode()); + System.out.println("Negotiated protocol (req1): " + ctx1.getProtocolVersion()); + System.out.println(r1.getBodyText()); + + // --- Request 2: defaults -> header omitted + final HttpClientContext ctx2 = HttpClientContext.create(); + ctx2.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.defaults()) + .build()); + + final SimpleHttpRequest req2 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/user-agent").build(); + final SimpleHttpResponse r2 = client.execute(req2, ctx2, null).get(); + System.out.println("[/httpbin/user-agent] -> " + r2.getCode()); + System.out.println("Negotiated protocol (req2): " + ctx2.getProtocolVersion()); + System.out.println(r2.getBodyText()); + + // --- Request 3: user-provided header -> preserved (no overwrite) + final HttpClientContext ctx3 = HttpClientContext.create(); + ctx3.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(5, false)) + .build()); + final SimpleHttpRequest req3 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/headers").build(); + req3.addHeader("Priority", "u=2"); + final SimpleHttpResponse r3 = client.execute(req3, ctx3, null).get(); + System.out.println("[/httpbin/headers with user header] -> " + r3.getCode()); + System.out.println("Negotiated protocol (req3): " + ctx3.getProtocolVersion()); + System.out.println(r3.getBodyText()); + } + } +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/H2RequestPriorityTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/H2RequestPriorityTest.java new file mode 100644 index 0000000000..ea775f577e --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/H2RequestPriorityTest.java @@ -0,0 +1,152 @@ +/* + * ==================================================================== + * 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.client5.http.protocol; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http2.priority.PriorityValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class H2RequestPriorityTest { + + private HttpClientContext h2ctx; + + @BeforeEach + void setUp() { + h2ctx = HttpClientContext.create(); + h2ctx.setProtocolVersion(HttpVersion.HTTP_2); + } + + @Test + void testH2RequestPriority_noopOnHttp11() throws Exception { + final HttpClientContext ctx11 = HttpClientContext.create(); + ctx11.setProtocolVersion(HttpVersion.HTTP_1_1); + ctx11.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(0, true)) + .build()); + + final BasicHttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + + final H2RequestPriority interceptor = H2RequestPriority.INSTANCE; + interceptor.process(request, null, ctx11); + + Assertions.assertNull(request.getFirstHeader(HttpHeaders.PRIORITY), + "No Priority header should be added for HTTP/1.1"); + } + + @Test + void adds_u_only_when_nonDefault_urgency() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(5, false)) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertNotNull(req.getFirstHeader(HttpHeaders.PRIORITY)); + Assertions.assertEquals("u=5", req.getFirstHeader(HttpHeaders.PRIORITY).getValue()); + Assertions.assertEquals(1, req.getHeaders(HttpHeaders.PRIORITY).length); + } + + @Test + void adds_i_only_when_incremental_true() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(3, true)) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertEquals("i", req.getFirstHeader(HttpHeaders.PRIORITY).getValue()); + } + + @Test + void adds_both_with_expected_format_and_order() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(1, true)) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertEquals("u=1, i", req.getFirstHeader(HttpHeaders.PRIORITY).getValue()); + } + + @Test + void omits_header_when_defaults() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.defaults()) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertNull(req.getFirstHeader(HttpHeaders.PRIORITY)); + } + + @Test + void preserves_existing_header() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + req.addHeader(HttpHeaders.PRIORITY, "u=0"); + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(5, true)) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertEquals("u=0", req.getFirstHeader(HttpHeaders.PRIORITY).getValue(), + "Existing header must be preserved"); + } + + @Test + void noop_when_no_configured_priority() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + // No RequestConfig set, or default without h2Priority. + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertNull(req.getFirstHeader(HttpHeaders.PRIORITY)); + } + + @Test + void respects_case_insensitive_existing_header_name() throws Exception { + final BasicHttpRequest req = new BasicHttpRequest("GET", new HttpHost("h"), "/"); + req.addHeader("priority", "u=6"); // lower-case, should still be found + h2ctx.setRequestConfig(RequestConfig.custom() + .setH2Priority(PriorityValue.of(0, true)) + .build()); + + H2RequestPriority.INSTANCE.process(req, null, h2ctx); + + Assertions.assertEquals("u=6", req.getFirstHeader(HttpHeaders.PRIORITY).getValue()); + } +}