Skip to content

Commit 3fbe450

Browse files
author
jvangaalen
committed
Store raw body in responseData and only decompress when responseBody is accessed
1 parent 47cbb05 commit 3fbe450

File tree

5 files changed

+105
-108
lines changed

5 files changed

+105
-108
lines changed

src/core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ dependencies {
114114
isTransitive = false
115115
}
116116
implementation("org.apache.xmlgraphics:xmlgraphics-commons")
117+
implementation("org.brotli:dec")
117118
implementation("org.freemarker:freemarker")
118119
implementation("org.jodd:jodd-core")
119120
implementation("org.jodd:jodd-props")

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

Lines changed: 93 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;
@@ -160,6 +168,8 @@ public class SampleResult implements Serializable, Cloneable, Searchable {
160168

161169
private byte[] responseData = EMPTY_BA;
162170

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

165175
private String label = "";// Never return null
@@ -217,7 +227,7 @@ public class SampleResult implements Serializable, Cloneable, Searchable {
217227

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

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

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

@@ -802,12 +833,12 @@ public byte[] getResponseData() {
802833
public String getResponseDataAsString() {
803834
try {
804835
if(responseDataAsString == null) {
805-
responseDataAsString= new String(responseData,getDataEncodingWithDefault());
836+
responseDataAsString= new String(getResponseData(),getDataEncodingWithDefault());
806837
}
807838
return responseDataAsString;
808839
} catch (UnsupportedEncodingException e) {
809840
log.warn("Using platform default as {} caused {}", getDataEncodingWithDefault(), e.getLocalizedMessage());
810-
return new String(responseData,Charset.defaultCharset()); // N.B. default charset is used deliberately here
841+
return new String(getResponseData(),Charset.defaultCharset()); // N.B. default charset is used deliberately here
811842
}
812843
}
813844

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

src/protocol/http/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ dependencies {
7373
implementation("dnsjava:dnsjava")
7474
implementation("org.apache.httpcomponents:httpmime")
7575
implementation("org.apache.httpcomponents:httpcore")
76-
implementation("org.brotli:dec")
7776
implementation("com.miglayout:miglayout-swing")
7877
implementation("com.fasterxml.jackson.core:jackson-core")
7978
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
@@ -57,7 +57,6 @@
5757
import org.apache.http.HttpRequest;
5858
import org.apache.http.HttpRequestInterceptor;
5959
import org.apache.http.HttpResponse;
60-
import org.apache.http.HttpResponseInterceptor;
6160
import org.apache.http.NameValuePair;
6261
import org.apache.http.StatusLine;
6362
import org.apache.http.auth.AuthSchemeProvider;
@@ -72,7 +71,6 @@
7271
import org.apache.http.client.config.AuthSchemes;
7372
import org.apache.http.client.config.CookieSpecs;
7473
import org.apache.http.client.config.RequestConfig;
75-
import org.apache.http.client.entity.InputStreamFactory;
7674
import org.apache.http.client.entity.UrlEncodedFormEntity;
7775
import org.apache.http.client.methods.CloseableHttpResponse;
7876
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
@@ -86,7 +84,6 @@
8684
import org.apache.http.client.methods.HttpTrace;
8785
import org.apache.http.client.methods.HttpUriRequest;
8886
import org.apache.http.client.protocol.HttpClientContext;
89-
import org.apache.http.client.protocol.ResponseContentEncoding;
9087
import org.apache.http.config.Lookup;
9188
import org.apache.http.config.Registry;
9289
import org.apache.http.config.RegistryBuilder;
@@ -147,8 +144,6 @@
147144
import org.apache.jmeter.protocol.http.control.DynamicKerberosSchemeFactory;
148145
import org.apache.jmeter.protocol.http.control.DynamicSPNegoSchemeFactory;
149146
import org.apache.jmeter.protocol.http.control.HeaderManager;
150-
import org.apache.jmeter.protocol.http.sampler.hc.LaxDeflateInputStream;
151-
import org.apache.jmeter.protocol.http.sampler.hc.LaxGZIPInputStream;
152147
import org.apache.jmeter.protocol.http.sampler.hc.LazyLayeredConnectionSocketFactory;
153148
import org.apache.jmeter.protocol.http.util.ConversionUtils;
154149
import org.apache.jmeter.protocol.http.util.HTTPArgument;
@@ -166,7 +161,6 @@
166161
import org.apache.jmeter.util.JsseSSLManager;
167162
import org.apache.jmeter.util.SSLManager;
168163
import org.apache.jorphan.util.JOrphanUtils;
169-
import org.brotli.dec.BrotliInputStream;
170164
import org.slf4j.Logger;
171165
import org.slf4j.LoggerFactory;
172166

@@ -195,20 +189,8 @@ public class HTTPHC4Impl extends HTTPHCAbstractImpl {
195189

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

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

204-
private static final InputStreamFactory GZIP =
205-
instream -> new LaxGZIPInputStream(instream, GZIP_RELAX_MODE);
206-
207-
private static final InputStreamFactory DEFLATE =
208-
instream -> new LaxDeflateInputStream(instream, DEFLATE_RELAX_MODE);
209-
210-
private static final InputStreamFactory BROTLI = BrotliInputStream::new;
211-
212194
private static final class ManagedCredentialsProvider implements CredentialsProvider {
213195
private final AuthManager authManager;
214196
private final Credentials proxyCredentials;
@@ -472,55 +454,6 @@ protected HttpResponse doSendRequest(
472454
}
473455
};
474456

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

561-
/**
562-
* Customize to plug Brotli
563-
* @return {@link Lookup}
564-
*/
565-
private static Lookup<InputStreamFactory> createLookupRegistry() {
566-
return
567-
RegistryBuilder.<InputStreamFactory>create()
568-
.register("br", BROTLI)
569-
.register("gzip", GZIP)
570-
.register("x-gzip", GZIP)
571-
.register("deflate", DEFLATE).build();
572-
}
573-
574494
/**
575495
* Implementation that allows GET method to have a body
576496
*/
@@ -675,7 +595,12 @@ protected HTTPSampleResult sample(URL url, String method,
675595
}
676596
HttpEntity entity = httpResponse.getEntity();
677597
if (entity != null) {
678-
res.setResponseData(readResponse(res, entity.getContent(), entity.getContentLength()));
598+
Header contentEncodingHeader = entity.getContentEncoding();
599+
if (contentEncodingHeader != null) {
600+
res.setResponseData(EntityUtils.toByteArray(entity), contentEncodingHeader.getValue());
601+
} else {
602+
res.setResponseData(EntityUtils.toByteArray(entity));
603+
}
679604
}
680605

681606
res.sampleEnd(); // Done with the sampling proper.
@@ -1157,7 +1082,7 @@ private MutableTriple<CloseableHttpClient, AuthState, PoolingHttpClientConnectio
11571082
}
11581083
builder.setDefaultCredentialsProvider(credsProvider);
11591084
}
1160-
builder.disableContentCompression().addInterceptorLast(RESPONSE_CONTENT_ENCODING);
1085+
builder.disableContentCompression(); // Disable automatic decompression
11611086
if(BASIC_AUTH_PREEMPTIVE) {
11621087
builder.addInterceptorFirst(PREEMPTIVE_AUTH_INTERCEPTOR);
11631088
} else {

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

Lines changed: 4 additions & 22 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.commons.io.input.CountingInputStream;
3736
import org.apache.jmeter.protocol.http.control.AuthManager;
@@ -240,15 +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());
242+
String contentEncoding = conn.getContentEncoding();
244243
CountingInputStream instream = null;
245244
try {
246245
instream = new CountingInputStream(conn.getInputStream());
247-
if (gzipped) {
248-
in = new GZIPInputStream(instream);
249-
} else {
250-
in = instream;
251-
}
246+
in = instream;
252247
} catch (IOException e) {
253248
if (! (e.getCause() instanceof FileNotFoundException))
254249
{
@@ -276,28 +271,15 @@ protected byte[] readResponse(HttpURLConnection conn, SampleResult res) throws I
276271
log.info("Error Response Code: {}", conn.getResponseCode());
277272
}
278273

279-
if (gzipped) {
280-
in = new GZIPInputStream(errorStream);
281-
} else {
282-
in = errorStream;
283-
}
284-
} catch (Exception e) {
285-
log.error("readResponse: {}", e.toString());
286-
Throwable cause = e.getCause();
287-
if (cause != null){
288-
log.error("Cause: {}", cause.toString());
289-
if(cause instanceof Error) {
290-
throw (Error)cause;
291-
}
292-
}
293-
in = conn.getErrorStream();
274+
in = errorStream;
294275
}
295276
// N.B. this closes 'in'
296277
byte[] responseData = readResponse(res, in, contentLength);
297278
if (instream != null) {
298279
res.setBodySize(instream.getByteCount());
299280
instream.close();
300281
}
282+
res.setResponseData(responseData, contentEncoding);
301283
return responseData;
302284
}
303285

0 commit comments

Comments
 (0)