Skip to content

Commit cacf8cb

Browse files
jvangaalenvlsi
authored andcommitted
Store raw body in responseData and only decompress when responseBody is accessed
1 parent 1f17592 commit cacf8cb

File tree

5 files changed

+100
-109
lines changed

5 files changed

+100
-109
lines changed

src/core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ dependencies {
111111
isTransitive = false
112112
}
113113
implementation("org.apache.xmlgraphics:xmlgraphics-commons")
114+
implementation("org.brotli:dec")
114115
implementation("org.freemarker:freemarker")
115116
implementation("org.jodd:jodd-core")
116117
implementation("org.jodd:jodd-props")

src/core/src/main/java/org/apache/jmeter/samplers/SampleResult.java

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
package org.apache.jmeter.samplers;
1919

20+
import java.io.ByteArrayInputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
2024
import java.io.Serializable;
2125
import java.io.UnsupportedEncodingException;
2226
import java.net.HttpURLConnection;
@@ -25,9 +29,13 @@
2529
import java.nio.charset.StandardCharsets;
2630
import java.util.ArrayList;
2731
import java.util.List;
32+
import java.util.Locale;
2833
import java.util.Set;
2934
import java.util.concurrent.ConcurrentHashMap;
3035
import java.util.concurrent.TimeUnit;
36+
import java.util.zip.GZIPInputStream;
37+
import java.util.zip.Inflater;
38+
import java.util.zip.InflaterInputStream;
3139

3240
import org.apache.jmeter.assertions.AssertionResult;
3341
import org.apache.jmeter.gui.Searchable;
@@ -161,6 +169,8 @@ public class SampleResult implements Serializable, Cloneable, Searchable {
161169

162170
private byte[] responseData = EMPTY_BA;
163171

172+
private String contentEncoding; // Stores gzip/deflate encoding if response is compressed
173+
164174
private String responseCode = "";// Never return null
165175

166176
private String label = "";// Never return null
@@ -218,7 +228,7 @@ public class SampleResult implements Serializable, Cloneable, Searchable {
218228

219229
// TODO do contentType and/or dataEncoding belong in HTTPSampleResult instead?
220230
private String dataEncoding;// (is this really the character set?) e.g.
221-
// ISO-8895-1, UTF-8
231+
// ISO-8895-1, UTF-8
222232

223233
private String contentType = ""; // e.g. text/html; charset=utf-8
224234

@@ -792,6 +802,22 @@ public void setResponseData(final String response, final String encoding) {
792802
* @return the responseData value (cannot be null)
793803
*/
794804
public byte[] getResponseData() {
805+
if (responseData == null) {
806+
return EMPTY_BA;
807+
}
808+
if (contentEncoding != null && responseData.length > 0) {
809+
try {
810+
return switch (contentEncoding.toLowerCase(Locale.ROOT)) {
811+
case "gzip" -> decompressGzip(responseData);
812+
case "x-gzip" -> decompressGzip(responseData);
813+
case "deflate" -> decompressDeflate(responseData);
814+
case "br" -> decompressBrotli(responseData);
815+
default -> responseData;
816+
};
817+
} catch (IOException e) {
818+
log.warn("Failed to decompress response data", e);
819+
}
820+
}
795821
return responseData;
796822
}
797823

@@ -803,12 +829,12 @@ public byte[] getResponseData() {
803829
public String getResponseDataAsString() {
804830
try {
805831
if(responseDataAsString == null) {
806-
responseDataAsString= new String(responseData,getDataEncodingWithDefault());
832+
responseDataAsString= new String(getResponseData(),getDataEncodingWithDefault());
807833
}
808834
return responseDataAsString;
809835
} catch (UnsupportedEncodingException e) {
810836
log.warn("Using platform default as {} caused {}", getDataEncodingWithDefault(), e.getLocalizedMessage());
811-
return new String(responseData,Charset.defaultCharset()); // N.B. default charset is used deliberately here
837+
return new String(getResponseData(),Charset.defaultCharset()); // N.B. default charset is used deliberately here
812838
}
813839
}
814840

@@ -1666,4 +1692,63 @@ public TestLogicalAction getTestLogicalAction() {
16661692
public void setTestLogicalAction(TestLogicalAction testLogicalAction) {
16671693
this.testLogicalAction = testLogicalAction;
16681694
}
1695+
1696+
/**
1697+
* Sets the response data and its compression encoding.
1698+
* @param data The response data
1699+
* @param encoding The content encoding (e.g. gzip, deflate)
1700+
*/
1701+
public void setResponseData(byte[] data, String encoding) {
1702+
responseData = data == null ? EMPTY_BA : data;
1703+
contentEncoding = encoding;
1704+
responseDataAsString = null;
1705+
}
1706+
1707+
private static byte[] decompressGzip(byte[] in) throws IOException {
1708+
try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(in));
1709+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
1710+
byte[] buf = new byte[8192];
1711+
int len;
1712+
while ((len = gis.read(buf)) > 0) {
1713+
out.write(buf, 0, len);
1714+
}
1715+
return out.toByteArray();
1716+
}
1717+
}
1718+
1719+
private static byte[] decompressDeflate(byte[] in) throws IOException {
1720+
// Try with ZLIB wrapper first
1721+
try {
1722+
return decompressWithInflater(in, false);
1723+
} catch (IOException e) {
1724+
// If that fails, try with NO_WRAP for raw DEFLATE
1725+
return decompressWithInflater(in, true);
1726+
}
1727+
}
1728+
1729+
private static byte[] decompressWithInflater(byte[] in, boolean nowrap) throws IOException {
1730+
try (InflaterInputStream iis = new InflaterInputStream(
1731+
new ByteArrayInputStream(in),
1732+
new Inflater(nowrap));
1733+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
1734+
byte[] buf = new byte[8192];
1735+
int len;
1736+
while ((len = iis.read(buf)) > 0) {
1737+
out.write(buf, 0, len);
1738+
}
1739+
return out.toByteArray();
1740+
}
1741+
}
1742+
1743+
private static byte[] decompressBrotli(byte[] in) throws IOException {
1744+
try (InputStream bis = new org.brotli.dec.BrotliInputStream(new ByteArrayInputStream(in));
1745+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
1746+
byte[] buf = new byte[8192];
1747+
int len;
1748+
while ((len = bis.read(buf)) > 0) {
1749+
out.write(buf, 0, len);
1750+
}
1751+
return out.toByteArray();
1752+
}
1753+
}
16691754
}

src/protocol/http/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ dependencies {
6363
implementation("dnsjava:dnsjava")
6464
implementation("org.apache.httpcomponents:httpmime")
6565
implementation("org.apache.httpcomponents:httpcore")
66-
implementation("org.brotli:dec")
6766
implementation("com.miglayout:miglayout-swing")
6867
implementation("com.fasterxml.jackson.core:jackson-core")
6968
implementation("com.fasterxml.jackson.core:jackson-databind")

src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java

Lines changed: 7 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import org.apache.http.HttpRequest;
5656
import org.apache.http.HttpRequestInterceptor;
5757
import org.apache.http.HttpResponse;
58-
import org.apache.http.HttpResponseInterceptor;
5958
import org.apache.http.NameValuePair;
6059
import org.apache.http.StatusLine;
6160
import org.apache.http.auth.AuthSchemeProvider;
@@ -70,7 +69,6 @@
7069
import org.apache.http.client.config.AuthSchemes;
7170
import org.apache.http.client.config.CookieSpecs;
7271
import org.apache.http.client.config.RequestConfig;
73-
import org.apache.http.client.entity.InputStreamFactory;
7472
import org.apache.http.client.entity.UrlEncodedFormEntity;
7573
import org.apache.http.client.methods.CloseableHttpResponse;
7674
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
@@ -84,7 +82,6 @@
8482
import org.apache.http.client.methods.HttpTrace;
8583
import org.apache.http.client.methods.HttpUriRequest;
8684
import org.apache.http.client.protocol.HttpClientContext;
87-
import org.apache.http.client.protocol.ResponseContentEncoding;
8885
import org.apache.http.config.Lookup;
8986
import org.apache.http.config.Registry;
9087
import org.apache.http.config.RegistryBuilder;
@@ -146,8 +143,6 @@
146143
import org.apache.jmeter.protocol.http.control.DynamicKerberosSchemeFactory;
147144
import org.apache.jmeter.protocol.http.control.DynamicSPNegoSchemeFactory;
148145
import org.apache.jmeter.protocol.http.control.HeaderManager;
149-
import org.apache.jmeter.protocol.http.sampler.hc.LaxDeflateInputStream;
150-
import org.apache.jmeter.protocol.http.sampler.hc.LaxGZIPInputStream;
151146
import org.apache.jmeter.protocol.http.sampler.hc.LazyLayeredConnectionSocketFactory;
152147
import org.apache.jmeter.protocol.http.util.ConversionUtils;
153148
import org.apache.jmeter.protocol.http.util.HTTPArgument;
@@ -165,7 +160,6 @@
165160
import org.apache.jmeter.util.SSLManager;
166161
import org.apache.jorphan.util.JOrphanUtils;
167162
import org.apache.jorphan.util.StringUtilities;
168-
import org.brotli.dec.BrotliInputStream;
169163
import org.slf4j.Logger;
170164
import org.slf4j.LoggerFactory;
171165

@@ -194,20 +188,8 @@ public class HTTPHC4Impl extends HTTPHCAbstractImpl {
194188

195189
private static final boolean DISABLE_DEFAULT_UA = JMeterUtils.getPropDefault("httpclient4.default_user_agent_disabled", false);
196190

197-
private static final boolean GZIP_RELAX_MODE = JMeterUtils.getPropDefault("httpclient4.gzip_relax_mode", false);
198-
199-
private static final boolean DEFLATE_RELAX_MODE = JMeterUtils.getPropDefault("httpclient4.deflate_relax_mode", false);
200-
201191
private static final Logger log = LoggerFactory.getLogger(HTTPHC4Impl.class);
202192

203-
private static final InputStreamFactory GZIP =
204-
instream -> new LaxGZIPInputStream(instream, GZIP_RELAX_MODE);
205-
206-
private static final InputStreamFactory DEFLATE =
207-
instream -> new LaxDeflateInputStream(instream, DEFLATE_RELAX_MODE);
208-
209-
private static final InputStreamFactory BROTLI = BrotliInputStream::new;
210-
211193
private static final class ManagedCredentialsProvider implements CredentialsProvider {
212194
private final AuthManager authManager;
213195
private final Credentials proxyCredentials;
@@ -464,55 +446,6 @@ protected HttpResponse doSendRequest(
464446
}
465447
};
466448

467-
private static final String[] HEADERS_TO_SAVE = new String[]{
468-
"content-length",
469-
"content-encoding",
470-
"content-md5"
471-
};
472-
473-
/**
474-
* Custom implementation that backups headers related to Compressed responses
475-
* that HC core {@link ResponseContentEncoding} removes after uncompressing
476-
* See Bug 59401
477-
*/
478-
@SuppressWarnings("UnnecessaryAnonymousClass")
479-
private static final HttpResponseInterceptor RESPONSE_CONTENT_ENCODING = new ResponseContentEncoding(createLookupRegistry()) {
480-
@Override
481-
public void process(HttpResponse response, HttpContext context)
482-
throws HttpException, IOException {
483-
ArrayList<Header[]> headersToSave = null;
484-
485-
final HttpEntity entity = response.getEntity();
486-
final HttpClientContext clientContext = HttpClientContext.adapt(context);
487-
final RequestConfig requestConfig = clientContext.getRequestConfig();
488-
// store the headers if necessary
489-
if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
490-
final Header ceheader = entity.getContentEncoding();
491-
if (ceheader != null) {
492-
headersToSave = new ArrayList<>(3);
493-
for(String name : HEADERS_TO_SAVE) {
494-
Header[] hdr = response.getHeaders(name); // empty if none
495-
headersToSave.add(hdr);
496-
}
497-
}
498-
}
499-
500-
// Now invoke original parent code
501-
super.process(response, clientContext);
502-
// Should this be in a finally ?
503-
if(headersToSave != null) {
504-
for (Header[] headers : headersToSave) {
505-
for (Header headerToRestore : headers) {
506-
if (response.containsHeader(headerToRestore.getName())) {
507-
break;
508-
}
509-
response.addHeader(headerToRestore);
510-
}
511-
}
512-
}
513-
}
514-
};
515-
516449
/**
517450
* 1 HttpClient instance per combination of (HttpClient,HttpClientKey)
518451
*/
@@ -549,19 +482,6 @@ protected HTTPHC4Impl(HTTPSamplerBase testElement) {
549482
super(testElement);
550483
}
551484

552-
/**
553-
* Customize to plug Brotli
554-
* @return {@link Lookup}
555-
*/
556-
private static Lookup<InputStreamFactory> createLookupRegistry() {
557-
return
558-
RegistryBuilder.<InputStreamFactory>create()
559-
.register("br", BROTLI)
560-
.register("gzip", GZIP)
561-
.register("x-gzip", GZIP)
562-
.register("deflate", DEFLATE).build();
563-
}
564-
565485
/**
566486
* Implementation that allows GET method to have a body
567487
*/
@@ -666,7 +586,12 @@ protected HTTPSampleResult sample(URL url, String method,
666586
}
667587
HttpEntity entity = httpResponse.getEntity();
668588
if (entity != null) {
669-
res.setResponseData(readResponse(res, entity.getContent(), entity.getContentLength()));
589+
Header contentEncodingHeader = entity.getContentEncoding();
590+
if (contentEncodingHeader != null) {
591+
res.setResponseData(EntityUtils.toByteArray(entity), contentEncodingHeader.getValue());
592+
} else {
593+
res.setResponseData(EntityUtils.toByteArray(entity));
594+
}
670595
}
671596

672597
res.sampleEnd(); // Done with the sampling proper.
@@ -1147,7 +1072,7 @@ private HttpClientState setupClient(HttpClientKey key, JMeterVariables jMeterVar
11471072
}
11481073
builder.setDefaultCredentialsProvider(credsProvider);
11491074
}
1150-
builder.disableContentCompression().addInterceptorLast(RESPONSE_CONTENT_ENCODING);
1075+
builder.disableContentCompression(); // Disable automatic decompression
11511076
if(BASIC_AUTH_PREEMPTIVE) {
11521077
builder.addInterceptorFirst(PREEMPTIVE_AUTH_INTERCEPTOR);
11531078
} else {

src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/sampler/HTTPJavaImpl.java

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.util.List;
3232
import java.util.Map;
3333
import java.util.function.Predicate;
34-
import java.util.zip.GZIPInputStream;
3534

3635
import org.apache.jmeter.protocol.http.control.AuthManager;
3736
import org.apache.jmeter.protocol.http.control.Authorization;
@@ -240,16 +239,11 @@ protected byte[] readResponse(HttpURLConnection conn, SampleResult res) throws I
240239
}
241240

242241
// works OK even if ContentEncoding is null
243-
boolean gzipped = HTTPConstants.ENCODING_GZIP.equals(conn.getContentEncoding());
244-
242+
String contentEncoding = conn.getContentEncoding();
245243
CountingInputStream instream = null;
246244
try {
247245
instream = new CountingInputStream(conn.getInputStream());
248-
if (gzipped) {
249-
in = new GZIPInputStream(instream);
250-
} else {
251-
in = instream;
252-
}
246+
in = instream;
253247
} catch (IOException e) {
254248
if (! (e.getCause() instanceof FileNotFoundException))
255249
{
@@ -277,28 +271,15 @@ protected byte[] readResponse(HttpURLConnection conn, SampleResult res) throws I
277271
log.info("Error Response Code: {}", conn.getResponseCode());
278272
}
279273

280-
if (gzipped) {
281-
in = new GZIPInputStream(errorStream);
282-
} else {
283-
in = errorStream;
284-
}
285-
} catch (Exception e) {
286-
log.error("readResponse: {}", e.toString());
287-
Throwable cause = e.getCause();
288-
if (cause != null){
289-
log.error("Cause: {}", cause.toString());
290-
if(cause instanceof Error error) {
291-
throw error;
292-
}
293-
}
294-
in = conn.getErrorStream();
274+
in = errorStream;
295275
}
296276
// N.B. this closes 'in'
297277
byte[] responseData = readResponse(res, in, contentLength);
298278
if (instream != null) {
299279
res.setBodySize(instream.getBytesRead());
300280
instream.close();
301281
}
282+
res.setResponseData(responseData, contentEncoding);
302283
return responseData;
303284
}
304285

0 commit comments

Comments
 (0)