diff --git a/pom.xml b/pom.xml
index aec704d11..f8844ec7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.xceptance
xlt
- 6.0.0
+ 6.1.0
jar
XLT
diff --git a/src/main/java/com/gargoylesoftware/htmlunit/HttpWebConnection.java b/src/main/java/com/gargoylesoftware/htmlunit/HttpWebConnection.java
index d8291d05a..1b67cec9d 100644
--- a/src/main/java/com/gargoylesoftware/htmlunit/HttpWebConnection.java
+++ b/src/main/java/com/gargoylesoftware/htmlunit/HttpWebConnection.java
@@ -297,7 +297,12 @@ private HttpUriRequest makeHttpMethod(final WebRequest webRequest, final HttpCli
setProxy(httpMethod, webRequest);
if (httpMethod instanceof HttpEntityEnclosingRequest) {
+ // start XC: GH#211
+ /*
// POST as well as PUT and PATCH
+ */
+ // POST, PUT, PATCH, and DELETE
+ // end XC: GH#211
final HttpEntityEnclosingRequest method = (HttpEntityEnclosingRequest) httpMethod;
if (webRequest.getEncodingType() == FormEncodingType.URL_ENCODED && method instanceof HttpPost) {
@@ -362,7 +367,12 @@ else if (FormEncodingType.MULTIPART == webRequest.getEncodingType()) {
}
method.setEntity(builder.build());
}
+ // start XC: GH#211
+ /*
else { // for instance a PUT or PATCH request
+ */
+ else { // PUT, PATCH, DELETE
+ // end XC: GH#211
final String body = webRequest.getRequestBody();
if (body != null) {
method.setEntity(new StringEntity(body, charset));
@@ -370,7 +380,12 @@ else if (FormEncodingType.MULTIPART == webRequest.getEncodingType()) {
}
}
else {
+ // start XC: GH#211
+ /*
// this is the case for GET as well as TRACE, DELETE, OPTIONS and HEAD
+ */
+ // GET, HEAD, OPTIONS, TRACE
+ // end XC: GH#211
if (!webRequest.getRequestParameters().isEmpty()) {
final List pairs = webRequest.getRequestParameters();
final String query = URLEncodedUtils.format(NameValuePair.toHttpClient(pairs), charset);
@@ -490,7 +505,7 @@ else if (pairWithFile.getFileName() == null) {
* @param uri the uri being used
* @return a new HttpClient HTTP method based on the specified parameters
*/
- private static HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri) {
+ protected HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri) {
final HttpRequestBase method;
switch (submitMethod) {
case GET:
diff --git a/src/main/java/com/gargoylesoftware/htmlunit/WebRequest.java b/src/main/java/com/gargoylesoftware/htmlunit/WebRequest.java
index f022d72aa..e65e3b499 100644
--- a/src/main/java/com/gargoylesoftware/htmlunit/WebRequest.java
+++ b/src/main/java/com/gargoylesoftware/htmlunit/WebRequest.java
@@ -423,10 +423,18 @@ public void setRequestBody(final String requestBody) throws RuntimeException {
+ "the two are mutually exclusive!";
throw new RuntimeException(msg);
}
+ // start XC: GH#211
+ /*
if (httpMethod_ != HttpMethod.POST && httpMethod_ != HttpMethod.PUT && httpMethod_ != HttpMethod.PATCH) {
final String msg = "The request body may only be set for POST, PUT or PATCH requests!";
throw new RuntimeException(msg);
}
+ */
+ if (httpMethod_ != HttpMethod.POST && httpMethod_ != HttpMethod.PUT && httpMethod_ != HttpMethod.PATCH && httpMethod_ != HttpMethod.DELETE) {
+ final String msg = "The request body may only be set for POST, PUT, PATCH, or DELETE requests!";
+ throw new RuntimeException(msg);
+ }
+ // end XC: GH#211
requestBody_ = requestBody;
}
diff --git a/src/main/java/com/xceptance/xlt/agentcontroller/FileManagerServlet.java b/src/main/java/com/xceptance/xlt/agentcontroller/FileManagerServlet.java
index 2ba6afb1b..aa3340f76 100644
--- a/src/main/java/com/xceptance/xlt/agentcontroller/FileManagerServlet.java
+++ b/src/main/java/com/xceptance/xlt/agentcontroller/FileManagerServlet.java
@@ -101,14 +101,12 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res
final File file = new File(rootDirectory, fileName);
in = new FileInputStream(file);
- resp.setContentLength((int) file.length());
- // resp.setContentType("???");
-
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentLengthLong(file.length());
+
final OutputStream out = resp.getOutputStream();
IOUtils.copy(in, out);
-
- resp.setStatus(HttpServletResponse.SC_OK);
}
}
catch (final Exception ex)
diff --git a/src/main/java/com/xceptance/xlt/engine/htmlunit/AbstractWebConnection.java b/src/main/java/com/xceptance/xlt/engine/htmlunit/AbstractWebConnection.java
index 7382b4fe0..cfe9d4fe8 100644
--- a/src/main/java/com/xceptance/xlt/engine/htmlunit/AbstractWebConnection.java
+++ b/src/main/java/com/xceptance/xlt/engine/htmlunit/AbstractWebConnection.java
@@ -161,7 +161,7 @@ private O makeRequest(final WebRequest webRequest) throws URISyntaxException
final O request;
// set parameters/body
- if (!(method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH))
+ if (!(method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH || method == HttpMethod.DELETE))
{
if (!webRequest.getRequestParameters().isEmpty())
{
@@ -226,10 +226,17 @@ else if (FormEncodingType.MULTIPART == webRequest.getEncodingType())
}
else
{
- // for instance a PUT or PATCH request
- final String body = StringUtils.defaultString(webRequest.getRequestBody());
+ // PUT, PATCH, DELETE
- request = createRequestWithStringBody(uri, webRequest, body, MimeType.TEXT_PLAIN, charset);
+ final String body = webRequest.getRequestBody();
+ if (body == null)
+ {
+ request = createRequestWithoutBody(uri, webRequest);
+ }
+ else
+ {
+ request = createRequestWithStringBody(uri, webRequest, body, MimeType.TEXT_PLAIN, charset);
+ }
}
}
diff --git a/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/HttpDeleteWithBody.java b/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/HttpDeleteWithBody.java
new file mode 100644
index 000000000..78c9b8712
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/HttpDeleteWithBody.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2005-2022 Xceptance Software Technologies GmbH
+ *
+ * Licensed 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.
+ */
+package com.xceptance.xlt.engine.htmlunit.apache;
+
+import java.net.URI;
+
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+/**
+ * Allows sending a DELETE request with or without a request body.
+ */
+class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase
+{
+ public HttpDeleteWithBody(final URI uri)
+ {
+ super();
+ setURI(uri);
+ }
+
+ @Override
+ public String getMethod()
+ {
+ return HttpDelete.METHOD_NAME;
+ }
+}
diff --git a/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/XltApacheHttpWebConnection.java b/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/XltApacheHttpWebConnection.java
index debf87010..3f14594d8 100644
--- a/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/XltApacheHttpWebConnection.java
+++ b/src/main/java/com/xceptance/xlt/engine/htmlunit/apache/XltApacheHttpWebConnection.java
@@ -16,6 +16,7 @@
package com.xceptance.xlt.engine.htmlunit.apache;
import java.io.IOException;
+import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -26,12 +27,14 @@
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.SocketConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import com.gargoylesoftware.htmlunit.DownloadedContent;
+import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
@@ -196,4 +199,25 @@ protected WebResponse makeWebResponse(final HttpResponse httpResponse, final Web
return webResponse;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri)
+ {
+ final HttpRequestBase method;
+
+ switch (submitMethod)
+ {
+ case DELETE:
+ method = new HttpDeleteWithBody(uri);
+ break;
+
+ default:
+ method = super.buildHttpMethod(submitMethod, uri);
+ }
+
+ return method;
+ }
}
diff --git a/src/main/java/com/xceptance/xlt/engine/httprequest/HttpRequest.java b/src/main/java/com/xceptance/xlt/engine/httprequest/HttpRequest.java
index 2a7e726a4..94f068313 100644
--- a/src/main/java/com/xceptance/xlt/engine/httprequest/HttpRequest.java
+++ b/src/main/java/com/xceptance/xlt/engine/httprequest/HttpRequest.java
@@ -617,13 +617,13 @@ public HttpRequest clone()
*/
protected WebRequest buildWebRequest() throws MalformedURLException, URISyntaxException
{
- final boolean isPostOrPutOrPatch = (httpMethod == HttpMethod.POST || httpMethod == HttpMethod.PUT ||
- httpMethod == HttpMethod.PATCH);
+ final boolean methodSupportsBody = (httpMethod == HttpMethod.POST || httpMethod == HttpMethod.PUT ||
+ httpMethod == HttpMethod.PATCH || httpMethod == HttpMethod.DELETE);
// basic parameter validation
Assert.assertTrue("Base URL must not be null or blank", StringUtils.isNotBlank(baseUrl));
- Assert.assertTrue("Can not use request parameters in conjunction with request body in POST, PUT or PATCH request",
- !isPostOrPutOrPatch || (body == null || parameters.isEmpty()));
+ Assert.assertTrue("Can not use request parameters in conjunction with request body in POST, PUT, PATCH, or DELETE requests",
+ !methodSupportsBody || (body == null && bytesBody == null) || parameters.isEmpty());
// Evaluate URL and create web request
final URL url;
@@ -650,7 +650,7 @@ protected WebRequest buildWebRequest() throws MalformedURLException, URISyntaxEx
webRequest.setCharset(contentCharset);
}
- if (isPostOrPutOrPatch && encodingType != null)
+ if (methodSupportsBody && encodingType != null)
{
webRequest.setEncodingType(encodingType);
}
@@ -661,10 +661,10 @@ protected WebRequest buildWebRequest() throws MalformedURLException, URISyntaxEx
}
// Handle parameters
- handleParameters(webRequest, parameters, isPostOrPutOrPatch);
+ handleParameters(webRequest, parameters, methodSupportsBody);
// Handle body
- if (isPostOrPutOrPatch)
+ if (methodSupportsBody)
{
// Assumes no parameters have been specified
@@ -701,15 +701,15 @@ else if (bytesBody != null)
* the web request
* @param parameters
* the custom request parameters
- * @param isPostOrPutOrPatch
- * whether the HTTP method is POST, PUT, or PATCH
+ * @param methodSupportsBody
+ * whether the HTTP method may have a request body
*/
- private void handleParameters(final WebRequest webRequest, final List parameters, final boolean isPostOrPutOrPatch)
+ private void handleParameters(final WebRequest webRequest, final List parameters, final boolean methodSupportsBody)
throws URISyntaxException, MalformedURLException
{
if (!parameters.isEmpty())
{
- if (isPostOrPutOrPatch)
+ if (methodSupportsBody)
{
// remove any parameter from the URL that is also part of the custom parameters
if (StringUtils.isNotEmpty(webRequest.getUrl().getQuery()))
diff --git a/src/main/java/com/xceptance/xlt/report/DataRecordReader.java b/src/main/java/com/xceptance/xlt/report/DataRecordReader.java
index dd7274289..676c7829b 100644
--- a/src/main/java/com/xceptance/xlt/report/DataRecordReader.java
+++ b/src/main/java/com/xceptance/xlt/report/DataRecordReader.java
@@ -21,7 +21,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListMap;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.GZIPInputStream;
import org.apache.commons.vfs2.FileObject;
@@ -70,7 +70,7 @@ class DataRecordReader implements Runnable
/**
* The global line counter.
*/
- private final AtomicInteger totalLineCounter;
+ private final AtomicLong totalLineCounter;
/**
* The instance number of the test user.
@@ -99,7 +99,7 @@ class DataRecordReader implements Runnable
* the dispatcher that coordinates result processing
*/
public DataRecordReader(final FileObject directory, final String agentName, final String testCaseName, final String userNumber,
- final AtomicInteger totalLineCounter, final Dispatcher dispatcher)
+ final AtomicLong totalLineCounter, final Dispatcher dispatcher)
{
this.directory = directory;
this.agentName = agentName;
diff --git a/src/main/java/com/xceptance/xlt/report/LogReader.java b/src/main/java/com/xceptance/xlt/report/LogReader.java
index 8fb10d271..5f1227e23 100644
--- a/src/main/java/com/xceptance/xlt/report/LogReader.java
+++ b/src/main/java/com/xceptance/xlt/report/LogReader.java
@@ -20,7 +20,7 @@
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileType;
@@ -82,7 +82,7 @@ public class LogReader
/**
* The total number of lines/data records read.
*/
- private final AtomicInteger totalLinesCounter;
+ private final AtomicLong totalLinesCounter;
/**
* The filter to skip the results of certain test cases when reading.
@@ -130,7 +130,7 @@ public LogReader(final FileObject inputDir, final DataRecordFactory dataRecordFa
{
this.inputDir = inputDir;
- totalLinesCounter = new AtomicInteger();
+ totalLinesCounter = new AtomicLong();
testCaseFilter = new StringMatcher(testCaseIncludePatternList, testCaseExcludePatternList, true);
agentFilter = new StringMatcher(agentIncludePatternList, agentExcludePatternList, true);
diff --git a/src/test/java/com/xceptance/xlt/engine/DeleteRequestWithBodyTest.java b/src/test/java/com/xceptance/xlt/engine/DeleteRequestWithBodyTest.java
new file mode 100644
index 000000000..6f97477f6
--- /dev/null
+++ b/src/test/java/com/xceptance/xlt/engine/DeleteRequestWithBodyTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2005-2022 Xceptance Software Technologies GmbH
+ *
+ * Licensed 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.
+ */
+package com.xceptance.xlt.engine;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.gargoylesoftware.htmlunit.HttpMethod;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+import com.xceptance.xlt.api.engine.Session;
+import com.xceptance.xlt.engine.httprequest.HttpRequest;
+import com.xceptance.xlt.engine.httprequest.HttpRequestHeaders;
+import com.xceptance.xlt.util.XltPropertiesImpl;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+/**
+ * Checks that DELETE requests may or may not have a body. To this end, a test server is set up, which provides the data
+ * received to let us verify the expectations.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class DeleteRequestWithBodyTest
+{
+ private static final Charset CONTENT_CHARSET = StandardCharsets.UTF_8;
+
+ private static final String CONTENT_TYPE = "application/json;charset=" + CONTENT_CHARSET;
+
+ private static final String CONTENT = "{ \"dummy\": \"äüö\" }";
+
+ private static final byte[] CONTENT_BYTES = CONTENT.getBytes(CONTENT_CHARSET);
+
+ /**
+ * The test server.
+ */
+ private static Server localServer;
+
+ /**
+ * The base URL of the test server.
+ */
+ private static String baseUrl;
+
+ /**
+ * The bytes received at the test server.
+ */
+ private static byte[] receivedBytes;
+
+ @BeforeClass
+ public static final void setUp() throws Exception
+ {
+ // create the local test server at any free port
+ localServer = new Server(0);
+
+ // register a handler that extracts the received data
+ localServer.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(final String target, final Request baseRequest, final HttpServletRequest request,
+ final HttpServletResponse response)
+ throws IOException, ServletException
+ {
+ final byte[] buffer = new byte[1024];
+ final int bytesRead = request.getInputStream().read(buffer);
+
+ if (bytesRead == -1)
+ {
+ receivedBytes = null;
+ }
+ else
+ {
+ receivedBytes = new byte[bytesRead];
+ System.arraycopy(buffer, 0, receivedBytes, 0, bytesRead);
+ }
+
+ baseRequest.setHandled(true);
+ }
+ });
+
+ // now start the server and build its URL
+ localServer.start();
+ baseUrl = localServer.getURI().toString();
+ }
+
+ @AfterClass
+ public static final void tearDown() throws Exception
+ {
+ XltPropertiesImpl.reset();
+ SessionImpl.removeCurrent();
+
+ localServer.stop();
+ localServer.destroy();
+ }
+
+ @After
+ public final void cleanUp() throws Exception
+ {
+ // clear the current session which in turn will close the default WebClient used by HttpRequest
+ Session.getCurrent().clear();
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ @Test
+ @Parameters(value =
+ {
+ "apache4|false|false", //
+ "apache4|false|true", //
+ "apache4|true|false", //
+ "apache4|true|true", //
+ "okhttp3|false|false", //
+ "okhttp3|false|true", //
+ "okhttp3|true|false", //
+ "okhttp3|true|true", //
+ })
+ public void delete(final String httpClientName, final boolean shouldHaveBody, final boolean useHttpRequest)
+ throws IOException, URISyntaxException
+ {
+ // choose the underlying HTTP client
+ XltPropertiesImpl.getInstance().setProperty("com.xceptance.xlt.http.client", httpClientName);
+
+ final HttpMethod method = HttpMethod.DELETE;
+
+ final WebResponse webResponse;
+ if (useHttpRequest)
+ {
+ // set up and execute request via HttpRequest
+ final HttpRequest httpRequest = new HttpRequest().timerName("foo").method(method).baseUrl(baseUrl).relativeUrl("/test");
+
+ if (shouldHaveBody)
+ {
+ httpRequest.header(HttpRequestHeaders.CONTENT_TYPE, CONTENT_TYPE).charset(StandardCharsets.UTF_8).body(CONTENT);
+ }
+
+ webResponse = httpRequest.fire().getWebResponse();
+ }
+ else
+ {
+ // set up and execute request via WebRequest/XltWebClient
+ final WebRequest webRequest = new WebRequest(new URL(baseUrl + "/test"), method);
+
+ if (shouldHaveBody)
+ {
+ webRequest.setAdditionalHeader(HttpRequestHeaders.CONTENT_TYPE, CONTENT_TYPE);
+ webRequest.setCharset(StandardCharsets.UTF_8);
+ webRequest.setRequestBody(CONTENT);
+ }
+
+ try (XltWebClient webClient = new XltWebClient())
+ {
+ webClient.setTimerName("foo");
+
+ webResponse = webClient.loadWebResponse(webRequest);
+ }
+ }
+
+ // validate response / request body content as received on the server
+ Assert.assertEquals(HttpStatus.OK_200, webResponse.getStatusCode());
+ Assert.assertArrayEquals(shouldHaveBody ? CONTENT_BYTES : null, receivedBytes);
+ }
+}
diff --git a/src/test/java/com/xceptance/xlt/engine/OkHttpRequestBodyEncodingTest.java b/src/test/java/com/xceptance/xlt/engine/OkHttpRequestBodyEncodingTest.java
index 71ef7e9a6..a206fd0a9 100644
--- a/src/test/java/com/xceptance/xlt/engine/OkHttpRequestBodyEncodingTest.java
+++ b/src/test/java/com/xceptance/xlt/engine/OkHttpRequestBodyEncodingTest.java
@@ -27,6 +27,7 @@
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -34,6 +35,7 @@
import org.junit.runner.RunWith;
import com.gargoylesoftware.htmlunit.HttpMethod;
+import com.xceptance.xlt.api.engine.Session;
import com.xceptance.xlt.engine.httprequest.HttpRequest;
import com.xceptance.xlt.engine.httprequest.HttpRequestHeaders;
import com.xceptance.xlt.engine.httprequest.HttpResponse;
@@ -129,6 +131,13 @@ public static final void tearDown() throws Exception
localServer.destroy();
}
+ @After
+ public final void cleanUp() throws Exception
+ {
+ // clear the current session which in turn will close the default WebClient used by HttpRequest
+ Session.getCurrent().clear();
+ }
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Test