Skip to content

Commit 802cfe2

Browse files
committed
POST h2c (http) with HTTP/2
1 parent d091bd5 commit 802cfe2

3 files changed

Lines changed: 191 additions & 16 deletions

File tree

src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,15 @@ public int getRequestTimeout() {
863863
return requestTimeout;
864864
}
865865

866+
/**
867+
* Whether a {@code protocol_error} may be retried over HTTP/1.1 with this client's resolved
868+
* configuration. Already {@code false} whenever HTTP/1.1 is disabled, so callers do not have to
869+
* pair it with a separate HTTP/1.1 check.
870+
*/
871+
public boolean isProtocolErrorFallbackEnabled() {
872+
return protocolErrorFallbackEnabled;
873+
}
874+
866875
public void loadProperties() {
867876
loadProperties(null);
868877
}
@@ -4003,13 +4012,44 @@ private HttpClient resolveClientForRequest(HTTP2Sampler sampler, HTTPSampleResul
40034012
throws URISyntaxException {
40044013
URI uri = result.getURL().toURI();
40054014
if ("http".equalsIgnoreCase(uri.getScheme())
4006-
&& shouldAttachRequestBody(sampler, result, false)) {
4015+
&& shouldAttachRequestBody(sampler, result, false)
4016+
&& canDivertCleartextBodyToHttp11(uri)) {
40074017
lowLevelDebug("Cleartext request with body; using HTTP/1.1-only client for {}", uri);
40084018
return httpClientHttp1Only;
40094019
}
40104020
return selectHttpClient(uri, isRecoverableIfHttp3Fails(sampler));
40114021
}
40124022

4023+
/**
4024+
* Whether a bodied cleartext request may be diverted to the HTTP/1.1-only client.
4025+
*
4026+
* <p>The diversion only exists to sidestep the h2c Upgrade dance, whose first request travels as
4027+
* plain HTTP/1.1 and which servers handle inconsistently when it carries a body. It is a
4028+
* shortcut around a negotiation, never a protocol choice, so it must not fire where HTTP/1.1 is
4029+
* not what the configuration asks for:
4030+
*
4031+
* <ul>
4032+
* <li>HTTP/1.1 disabled: no request may go out as HTTP/1.1, bodied or not. Sending one to an
4033+
* h2c origin makes the server answer with HTTP/2 frames that the HTTP/1.1 parser reads as
4034+
* garbage ({@code Illegal character CNTL=0x0}).</li>
4035+
* <li>h2c prior knowledge (configured, or learned and still cached): the origin is spoken to
4036+
* as HTTP/2 from the first byte, so there is no Upgrade to avoid in the first place.</li>
4037+
* </ul>
4038+
*/
4039+
private boolean canDivertCleartextBodyToHttp11(URI uri) {
4040+
if (!enableHttp1) {
4041+
lowLevelDebug("Cleartext request with body but HTTP/1.1 is disabled; "
4042+
+ "keeping protocol selection for {}", uri);
4043+
return false;
4044+
}
4045+
if (shouldUseH2cPriorKnowledge(uri)) {
4046+
lowLevelDebug("Cleartext request with body on an h2c prior-knowledge origin; "
4047+
+ "keeping HTTP/2 for {}", uri);
4048+
return false;
4049+
}
4050+
return true;
4051+
}
4052+
40134053
/**
40144054
* Whether a failed HTTP/3 attempt for this request could still be served over another protocol.
40154055
* Used when deciding fallback after an indicated HTTP/3 failure (cached Alt-Svc / prior

src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -476,25 +476,31 @@ protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRe
476476
if ((isProtocolErrorCause || isProtocolErrorException)
477477
&& !HpackFailureDetector.indicatesHpackFailure(e)
478478
&& !HpackFailureDetector.indicatesHpackFailure(cause)) {
479-
boolean fallbackEnabled = isProtocolErrorFallbackEnabled();
479+
HTTP2JettyClient fallbackClient = resolveClientForFallback();
480+
// The client owns the resolved answer: it already merges this sampler's own flags with
481+
// the JMeter properties, and it already turns the fallback off when HTTP/1.1 is disabled.
482+
// Re-deriving it from properties alone here ignored both, and retried over HTTP/1.1 even
483+
// against h2c-only endpoints where no request may go out as HTTP/1.1.
484+
boolean fallbackEnabled =
485+
fallbackClient != null && fallbackClient.isProtocolErrorFallbackEnabled();
480486
if (!fallbackEnabled) {
481487
LOG.warn("HTTP/2 protocol_error detected and fallback is DISABLED. "
482488
+ "Request will fail.");
483489
LOG.warn("Error: {}", cause != null ? cause.getMessage() : e.getMessage());
484490
LOG.warn("To enable fallback, set blazemeter.http.protocolErrorFallbackEnabled=true "
485-
+ "or blazemeter.http.disableFallback=false in user.properties or jmeter.properties");
491+
+ "or blazemeter.http.disableFallback=false in user.properties or jmeter.properties, "
492+
+ "and keep HTTP/1.1 enabled on the sampler");
486493
} else {
487494
LOG.warn("HTTP/2 protocol_error detected. Attempting fallback to HTTP/1.1");
488495
LOG.warn("Error: {}", cause != null ? cause.getMessage() : e.getMessage());
489496

490497
try {
491-
// Get the client and request details for fallback
492-
HTTP2JettyClient client = clientFactory.call();
493498
HTTPSampleResult fallbackBase = resolveErrorResult(preparedResult, url, method);
494499

495500
// Retry with HTTP/1.1 only
496501
LOG.info("Retrying request with HTTP/1.1 only: {}", url);
497-
HTTPSampleResult fallbackResult = client.retryWithHTTP11Only(this, fallbackBase);
502+
HTTPSampleResult fallbackResult =
503+
fallbackClient.retryWithHTTP11Only(this, fallbackBase);
498504

499505
if (fallbackResult != null && fallbackResult.isSuccessful()) {
500506
LOG.info("HTTP/1.1 fallback succeeded: status={}", fallbackResult.getResponseCode());
@@ -523,17 +529,17 @@ private HTTPSampleResult resolveErrorResult(HTTPSampleResult preparedResult, URL
523529
return buildResult(url, method);
524530
}
525531

526-
private boolean isProtocolErrorFallbackEnabled() {
527-
String fb =
528-
BzmHttpPluginProperties.resolveRaw("httpJettyClient.protocolErrorFallbackEnabled");
529-
if (fb != null) {
530-
return Boolean.parseBoolean(fb);
531-
}
532-
String df = BzmHttpPluginProperties.resolveRaw("httpJettyClient.disableFallback");
533-
if (df != null) {
534-
return !Boolean.parseBoolean(df);
532+
/**
533+
* The client that would serve this sampler, or {@code null} when it cannot be obtained. Used
534+
* from the failure path, where losing the client means there is nothing left to retry on.
535+
*/
536+
private HTTP2JettyClient resolveClientForFallback() {
537+
try {
538+
return clientFactory.call();
539+
} catch (Exception e) {
540+
LOG.error("Could not obtain the client to evaluate the HTTP/1.1 fallback", e);
541+
return null;
535542
}
536-
return true;
537543
}
538544

539545
protected Request sampleAsync(HTTPSampleResult result, HTTP2FutureResponseListener listener)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.blazemeter.jmeter.http2.core;
2+
3+
import static org.junit.Assert.assertSame;
4+
5+
import com.blazemeter.jmeter.http2.HTTP2TestBase;
6+
import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
7+
import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
import java.net.URI;
11+
import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
12+
import org.apache.jmeter.protocol.http.util.HTTPConstants;
13+
import org.junit.Before;
14+
import org.junit.BeforeClass;
15+
import org.junit.Test;
16+
17+
/**
18+
* Pins which client a bodied cleartext request gets.
19+
*
20+
* <p>Such requests are diverted to the HTTP/1.1-only client to sidestep the h2c Upgrade dance,
21+
* whose first request travels as plain HTTP/1.1 and which servers handle inconsistently when it
22+
* carries a body. That is a shortcut around a negotiation, not a protocol choice, so it must not
23+
* survive where HTTP/1.1 is disabled or where h2c is spoken from the first byte anyway — issue
24+
* #157, where POSTs to an h2c-only endpoint went out as HTTP/1.1 and the server's HTTP/2 frames
25+
* came back through the HTTP/1.1 parser as {@code Illegal character CNTL=0x0}.
26+
*/
27+
public class CleartextBodyClientSelectionTest extends HTTP2TestBase {
28+
29+
private static final URI CLEARTEXT_URI = URI.create("http://h2c-only.example.com:8000/policies");
30+
31+
private HTTP2JettyClient client;
32+
33+
@BeforeClass
34+
public static void setupClass() {
35+
JMeterTestUtils.setupJmeterEnv();
36+
}
37+
38+
@Before
39+
public void setUp() throws Exception {
40+
HTTP2JettyClient.clearStaticProtocolCaches();
41+
client = new HTTP2JettyClient();
42+
setBoolean("enableHttp1", true);
43+
setBoolean("enableHttp2", true);
44+
setBoolean("enableHttp3", false);
45+
setBoolean("http1UpgradeRequired", false);
46+
setBoolean("http2PriorKnowledgeEnabled", false);
47+
setBoolean("http3PriorKnowledgeEnabled", false);
48+
setBoolean("h2cCacheEnabled", false);
49+
}
50+
51+
@Test
52+
public void shouldUseHttp1OnlyForCleartextPostWhenUpgradeIsTheAlternative() throws Exception {
53+
setBoolean("http1UpgradeRequired", true);
54+
55+
assertSame("a bodied cleartext POST must skip the h2c Upgrade dance",
56+
field("httpClientHttp1Only"), resolveClientForRequest(post()));
57+
}
58+
59+
@Test
60+
public void shouldNotUseHttp1OnlyForCleartextPostWhenHttp1IsDisabled() throws Exception {
61+
setBoolean("enableHttp1", false);
62+
setBoolean("http1UpgradeRequired", true);
63+
64+
assertSame("with HTTP/1.1 disabled a bodied POST must still be spoken as h2c",
65+
field("httpClientH2cPrior"), resolveClientForRequest(post()));
66+
}
67+
68+
@Test
69+
public void shouldNotUseHttp1OnlyForCleartextPostWithH2cPriorKnowledge() throws Exception {
70+
setBoolean("http2PriorKnowledgeEnabled", true);
71+
72+
assertSame("prior knowledge means there is no Upgrade to avoid",
73+
field("httpClientH2cPrior"), resolveClientForRequest(post()));
74+
}
75+
76+
@Test
77+
public void shouldUseHttp1OnlyForCleartextGetSendingParametersAsBody() throws Exception {
78+
setBoolean("http1UpgradeRequired", true);
79+
HTTP2Sampler sampler = sampler(HTTPConstants.GET);
80+
sampler.setPostBodyRaw(true);
81+
82+
assertSame(field("httpClientHttp1Only"),
83+
resolveClientForRequest(sampler, result(HTTPConstants.GET)));
84+
}
85+
86+
private HTTP2Sampler post() {
87+
HTTP2Sampler sampler = sampler(HTTPConstants.POST);
88+
sampler.addArgument("policy", "value");
89+
return sampler;
90+
}
91+
92+
private HTTP2Sampler sampler(String method) {
93+
HTTP2Sampler sampler = new HTTP2Sampler();
94+
sampler.setMethod(method);
95+
return sampler;
96+
}
97+
98+
private HTTPSampleResult result(String method) throws Exception {
99+
HTTPSampleResult result = new HTTPSampleResult();
100+
result.setURL(CLEARTEXT_URI.toURL());
101+
result.setHTTPMethod(method);
102+
return result;
103+
}
104+
105+
private Object resolveClientForRequest(HTTP2Sampler sampler) throws Exception {
106+
return resolveClientForRequest(sampler, result(HTTPConstants.POST));
107+
}
108+
109+
private Object resolveClientForRequest(HTTP2Sampler sampler, HTTPSampleResult result)
110+
throws Exception {
111+
Method m = HTTP2JettyClient.class.getDeclaredMethod(
112+
"resolveClientForRequest", HTTP2Sampler.class, HTTPSampleResult.class);
113+
m.setAccessible(true);
114+
return m.invoke(client, sampler, result);
115+
}
116+
117+
private Object field(String name) throws Exception {
118+
Field f = HTTP2JettyClient.class.getDeclaredField(name);
119+
f.setAccessible(true);
120+
return f.get(client);
121+
}
122+
123+
private void setBoolean(String name, boolean value) throws Exception {
124+
Field f = HTTP2JettyClient.class.getDeclaredField(name);
125+
f.setAccessible(true);
126+
f.setBoolean(client, value);
127+
}
128+
129+
}

0 commit comments

Comments
 (0)