Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ public HTTPSampleResult retryWithHTTP11Only(HTTP2Sampler sampler, HTTPSampleResu
}
ensureHostHeader(http11Request, url);

configureContentDecoders(httpClientHttp1Only, http11Request);
configureContentDecodersAndCapture(httpClientHttp1Only, http11Request);

// Copy body if present
setBody(http11Request, sampler, result);
Expand Down Expand Up @@ -1200,7 +1200,7 @@ private ContentResponse sendWithHTTP11Only(Request originalRequest,
}
ensureHostHeader(http11Request, uri);

configureContentDecoders(httpClientHttp1Only, http11Request);
configureContentDecodersAndCapture(httpClientHttp1Only, http11Request);

// Copy body if present
if (originalRequest.getBody() != null) {
Expand Down Expand Up @@ -1254,7 +1254,7 @@ private ContentResponse sendWithH2cPriorKnowledge(Request originalRequest)
}
}
ensureHostHeader(h2cRequest, uri);
configureContentDecoders(httpClientH2cPrior, h2cRequest);
configureContentDecodersAndCapture(httpClientH2cPrior, h2cRequest);
if (originalRequest.getBody() != null) {
h2cRequest.body(originalRequest.getBody());
}
Expand Down Expand Up @@ -1299,7 +1299,7 @@ private void samplePrepareRequest(Request request,
addPreemptiveAuthorizationHeader(request, url, sampler.getAuthManager());
lowLevelDebug("Headers set, request URI: {}", request.getURI());

configureContentDecoders(client, request);
configureContentDecodersAndCapture(client, request);

String ae = request.getHeaders() != null
? request.getHeaders().get(HttpHeader.ACCEPT_ENCODING)
Expand Down Expand Up @@ -2105,6 +2105,11 @@ private void configureContentDecoders(HttpClient client, Request request) {
}
}

private void configureContentDecodersAndCapture(HttpClient client, Request request) {
configureContentDecoders(client, request);
JmeterCompressionHeadersSupport.installCapture(request);
}

private HttpClient selectHttpClient(URI uri) {
if (uri != null && "http".equalsIgnoreCase(uri.getScheme())) {
if (!enableHttp2 && enableHttp1) {
Expand Down Expand Up @@ -2511,7 +2516,7 @@ private Request cloneRequest(Request originalRequest, HttpClient client)
if (originalRequest.getBody() != null) {
request.body(originalRequest.getBody());
}
configureContentDecoders(client, request);
configureContentDecodersAndCapture(client, request);
return request;
}

Expand Down Expand Up @@ -3637,18 +3642,21 @@ private void setResultContentResponse(HTTPSampleResult result,
result.setURL(contentResponse.getRequest().getURI().toURL());
}

HttpFields sampleResultHeaders =
JmeterCompressionHeadersSupport.headersForSampleResult(contentResponse);
long headerBytes =
(long) result.getResponseHeaders().length() // condensed length (without \r)
+ (long) contentResponse.getHeaders().asString().length() // Add \r for each header
+ (long) sampleResultHeaders.asString().length() // Add \r for each header
+ 1L // Add \r for initial header
+ 2L; // final \r\n before data
result.setHeadersSize((int) headerBytes);
}

private String extractResponseHeaders(ContentResponse contentResponse,
String message) {
HttpFields headers = JmeterCompressionHeadersSupport.headersForSampleResult(contentResponse);
return contentResponse.getVersion() + " " + contentResponse.getStatus() + " " + message + "\n"
+ buildHeadersString(contentResponse.getHeaders());
+ buildHeadersString(headers);
}

private String extractRedirectLocation(ContentResponse contentResponse) {
Expand Down Expand Up @@ -3738,6 +3746,8 @@ private byte[] maybeDecodeCompressedContent(ContentResponse contentResponse) {
if (contentEncoding == null || contentEncoding.trim().isEmpty()) {
return content;
}
JmeterCompressionHeadersSupport.captureIfCompressed(
contentResponse.getRequest(), contentResponse.getHeaders());
String encodingToken = normalizeEncodingToken(contentEncoding);
if (encodingToken.isEmpty()) {
return content;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.blazemeter.jmeter.http2.core;

import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;

/**
* Preserves wire {@code Content-Encoding}, {@code Content-Length} and {@code Content-MD5}
* for compressed responses, matching Apache JMeter {@code HTTPHC4Impl} behaviour after
* HttpClient {@code ResponseContentEncoding} decodes the body (Bug 59401).
*/
final class JmeterCompressionHeadersSupport {

static final String REQUEST_ATTR_WIRE_COMPRESSION_HEADERS =
"bzm.jmeter.wireCompressionHeaders";

private static final String[] HEADERS_TO_SAVE = {
HTTPConstants.HEADER_CONTENT_LENGTH,
HTTPConstants.HEADER_CONTENT_ENCODING,
"Content-MD5"
};

private JmeterCompressionHeadersSupport() {
}

static void installCapture(Request request) {
if (request == null) {
return;
}
request.onResponseHeader(JmeterCompressionHeadersSupport::onResponseHeader);
request.onResponseHeaders(JmeterCompressionHeadersSupport::onResponseHeaders);
}

private static boolean onResponseHeader(Response response, HttpField field) {
if (response != null && field != null && HttpHeader.CONTENT_ENCODING.is(field.getName())) {
Request request = response.getRequest();
if (request != null) {
mergeSavedHeader(request, field.getName(), field.getValue());
}
}
return true;
}

private static void onResponseHeaders(Response response) {
if (response != null && response.getRequest() != null && response.getHeaders() != null) {
captureIfCompressed(response.getRequest(), response.getHeaders());
}
}

private static void mergeSavedHeader(Request request, String name, String value) {
if (request == null || name == null || value == null) {
return;
}
HttpFields.Mutable saved = getOrCreateSaved(request);
saved.put(name, value);
request.attribute(REQUEST_ATTR_WIRE_COMPRESSION_HEADERS, saved);
}

private static HttpFields.Mutable getOrCreateSaved(Request request) {
Object existing = request.getAttributes().get(REQUEST_ATTR_WIRE_COMPRESSION_HEADERS);
if (existing instanceof HttpFields.Mutable) {
return (HttpFields.Mutable) existing;
}
if (existing instanceof HttpFields) {
HttpFields.Mutable copy = HttpFields.build((HttpFields) existing);
request.attribute(REQUEST_ATTR_WIRE_COMPRESSION_HEADERS, copy);
return copy;
}
return HttpFields.build();
}

static void captureIfCompressed(Request request, HttpFields headers) {
if (request == null || headers == null) {
return;
}
String contentEncoding = headers.get(HttpHeader.CONTENT_ENCODING);
if (contentEncoding == null || contentEncoding.trim().isEmpty()) {
return;
}
HttpFields.Mutable saved = getOrCreateSaved(request);
for (String name : HEADERS_TO_SAVE) {
String value = headers.get(name);
if (value != null) {
saved.put(name, value);
}
}
if (saved.size() > 0) {
request.attribute(REQUEST_ATTR_WIRE_COMPRESSION_HEADERS, saved);
}
}

static HttpFields headersForSampleResult(ContentResponse contentResponse) {
if (contentResponse == null || contentResponse.getHeaders() == null) {
return contentResponse != null ? contentResponse.getHeaders() : HttpFields.EMPTY;
}
HttpFields current = contentResponse.getHeaders();
Request request = contentResponse.getRequest();
if (request == null) {
return current;
}
Object savedAttr = request.getAttributes().get(REQUEST_ATTR_WIRE_COMPRESSION_HEADERS);
if (!(savedAttr instanceof HttpFields)) {
return current;
}
HttpFields saved = (HttpFields) savedAttr;
if (saved.size() == 0) {
return current;
}
HttpFields.Mutable merged = HttpFields.build(current);
// Jetty removes Content-Encoding but often leaves an updated Content-Length (decoded size).
// Match HTTPHC4Impl: restore all wire compression headers when encoding was stripped.
if (!hasContentEncoding(current) && hasContentEncoding(saved)) {
for (String name : HEADERS_TO_SAVE) {
String value = saved.get(name);
if (value != null) {
merged.put(name, value);
}
}
} else {
for (String name : HEADERS_TO_SAVE) {
if (merged.contains(name)) {
continue;
}
String value = saved.get(name);
if (value != null) {
merged.put(name, value);
}
}
}
return merged;
}

private static boolean hasContentEncoding(HttpFields headers) {
if (headers == null) {
return false;
}
String contentEncoding = headers.get(HttpHeader.CONTENT_ENCODING);
return contentEncoding != null && !contentEncoding.trim().isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,8 @@ public void shouldReturnSuccessSampleResultWhenSuccessResponseWithContentTypeGzi
sampler.setHeaderManager(hm);
try {
HTTPSampleResult result = sampleWithGet(SERVER_PATH_200_GZIP);
boolean hasGzipHeader = result.getResponseHeaders().contains("Content-Encoding: gzip");
boolean hasBody = result.getResponseData() != null && result.getResponseData().length > 0;
assertThat(hasGzipHeader || hasBody).isTrue();
assertThat(result.getResponseHeaders()).containsIgnoringCase("content-encoding: gzip");
assertThat(result.getResponseData()).containsExactly(BINARY_RESPONSE_BODY);
} finally {
if (originalEnableHttp1 == null) {
JMeterUtils.getJMeterProperties().remove("httpJettyClient.enableHttp1");
Expand All @@ -300,8 +299,8 @@ public void shouldReturnSuccessSampleResultWhenSuccessResponseWithContentTypeBro
hm.add(new Header(HttpHeader.ACCEPT_ENCODING.asString(), "br"));
sampler.setHeaderManager(hm);
HTTPSampleResult result = sampleWithGet(SERVER_PATH_200_BROTLI);
// Verify that the request was successful and content was decompressed
assertThat(result.isSuccessful()).isTrue();
assertThat(result.getResponseHeaders()).containsIgnoringCase("content-encoding: br");
assertThat(result.getResponseData()).containsExactly(BINARY_RESPONSE_BODY);
}

Expand All @@ -313,8 +312,8 @@ public void shouldReturnSuccessSampleResultWhenSuccessResponseWithContentTypeZst
hm.add(new Header(HttpHeader.ACCEPT_ENCODING.asString(), "zstd"));
sampler.setHeaderManager(hm);
HTTPSampleResult result = sampleWithGet(SERVER_PATH_200_ZSTD);
// Verify that the request was successful and content was decompressed
assertThat(result.isSuccessful()).isTrue();
assertThat(result.getResponseHeaders()).containsIgnoringCase("content-encoding: zstd");
assertThat(result.getResponseData()).containsExactly(BINARY_RESPONSE_BODY);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.blazemeter.jmeter.http2.core;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;

import java.util.HashMap;
import java.util.Map;
import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.junit.Test;
import org.mockito.Mockito;

public class JmeterCompressionHeadersSupportTest {

private static Request mockRequestWithAttributes() {
Map<String, Object> attributes = new HashMap<>();
Request request = Mockito.mock(Request.class);
Mockito.when(request.getAttributes()).thenReturn(attributes);
Mockito.doAnswer(invocation -> {
attributes.put(invocation.getArgument(0), invocation.getArgument(1));
return invocation.getMock();
}).when(request).attribute(anyString(), any());
return request;
}

@Test
public void shouldRestoreWireCompressionHeadersRemovedByDecoder() {
Request request = mockRequestWithAttributes();

HttpFields wire = HttpFields.build()
.add(HttpHeader.CONTENT_ENCODING, "gzip")
.add(HttpHeader.CONTENT_LENGTH, "643");
JmeterCompressionHeadersSupport.captureIfCompressed(request, wire);

ContentResponse contentResponse = Mockito.mock(ContentResponse.class);
HttpFields decoded = HttpFields.build()
.add(HttpHeader.CONTENT_TYPE, "application/json")
.add(HttpHeader.CONTENT_LENGTH, "238");
Mockito.when(contentResponse.getRequest()).thenReturn(request);
Mockito.when(contentResponse.getHeaders()).thenReturn(decoded);

HttpFields merged = JmeterCompressionHeadersSupport.headersForSampleResult(contentResponse);

assertThat(merged.get(HttpHeader.CONTENT_ENCODING)).isEqualTo("gzip");
assertThat(merged.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("643");
assertThat(merged.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
}

@Test
public void shouldNotOverrideExistingHeaders() {
Request request = mockRequestWithAttributes();

HttpFields wire = HttpFields.build()
.add(HttpHeader.CONTENT_ENCODING, "gzip")
.add(HttpHeader.CONTENT_LENGTH, "643");
JmeterCompressionHeadersSupport.captureIfCompressed(request, wire);

ContentResponse contentResponse = Mockito.mock(ContentResponse.class);
HttpFields decoded = HttpFields.build()
.add(HttpHeader.CONTENT_ENCODING, "gzip")
.add(HttpHeader.CONTENT_LENGTH, "238");
Mockito.when(contentResponse.getRequest()).thenReturn(request);
Mockito.when(contentResponse.getHeaders()).thenReturn(decoded);

HttpFields merged = JmeterCompressionHeadersSupport.headersForSampleResult(contentResponse);

assertThat(merged.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("238");
}

@Test
public void shouldCaptureContentMd5WhenPresent() {
Request request = mockRequestWithAttributes();

HttpFields wire = HttpFields.build()
.add(HttpHeader.CONTENT_ENCODING, "br")
.add(HTTPConstants.HEADER_CONTENT_LENGTH, "100")
.add("Content-MD5", "abc123");
JmeterCompressionHeadersSupport.captureIfCompressed(request, wire);

ContentResponse contentResponse = Mockito.mock(ContentResponse.class);
HttpFields decoded = HttpFields.build().add(HttpHeader.CONTENT_TYPE, "text/plain");
Mockito.when(contentResponse.getRequest()).thenReturn(request);
Mockito.when(contentResponse.getHeaders()).thenReturn(decoded);

HttpFields merged = JmeterCompressionHeadersSupport.headersForSampleResult(contentResponse);

assertThat(merged.get(HttpHeader.CONTENT_ENCODING)).isEqualTo("br");
assertThat(merged.get(HTTPConstants.HEADER_CONTENT_LENGTH)).isEqualTo("100");
assertThat(merged.get("Content-MD5")).isEqualTo("abc123");
}
}