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