() {
+ @Override
+ public void completed(SimpleHttpResponse simpleHttpResponse) {
+ responseFuture.complete(simpleHttpResponse);
+ }
+
+ @Override
+ public void failed(Exception e) {
+ responseFuture.completeExceptionally(e);
+ }
+
+ @Override
+ public void cancelled() {
+ responseFuture.cancel(true);
+ }
+ });
+
+ responseFuture.get();
+ }
+
+
+ @Test
+ public void testSpanFinishOnEarlyException() throws Exception {
+
+ client.close(); //this forces execute to immediately exit with an exception
+
+ reporter.disableCheckServiceTarget();
+ reporter.disableCheckDestinationAddress();
+ try {
+ assertThatThrownBy(() -> performGet(getBaseUrl() + "/")).cause().isInstanceOf(IllegalStateException.class);
+ } finally {
+ //Reset state for other tests
+ setUp();
+ reporter.resetChecks();
+ }
+ assertThat(reporter.getFirstSpan(500)).isNotNull();
+ assertThat(reporter.getSpans()).hasSize(1);
+ }
+
+ @Test
+ public void testSpanFinishWithIllegalProtocol() throws Exception {
+ reporter.disableCheckServiceTarget();
+ reporter.disableCheckDestinationAddress();
+ String url = getBaseUrl().replaceAll("http", "ottp") + "/";
+ performGet(url);
+
+ Span firstSpan = reporter.getFirstSpan(500);
+ assertThat(firstSpan).isNotNull();
+ assertThat(firstSpan.getOutcome()).isEqualTo(Outcome.FAILURE);
+ assertThat(firstSpan.getNameAsString()).isEqualTo("GET localhost");
+ assertThat(reporter.getSpans()).hasSize(1);
+ }
+
+ @Test
+ public void testSpanFinishWithIllegalUrl() throws Exception {
+ reporter.disableCheckServiceTarget();
+ reporter.disableCheckDestinationAddress();
+ String url = getBaseUrl().replaceAll("http:/", "") + "/";
+
+ try {
+ assertThatThrownBy(() -> performGet(url)).cause().isInstanceOf(ProtocolException.class);
+ } finally {
+ //Reset state for other tests
+ setUp();
+ reporter.resetChecks();
+ }
+
+ Span firstSpan = reporter.getFirstSpan(500);
+ assertThat(firstSpan).isNotNull();
+ assertThat(firstSpan.getOutcome()).isEqualTo(Outcome.FAILURE);
+ assertThat(firstSpan.getNameAsString()).isEqualTo("GET ");
+ assertThat(reporter.getSpans()).hasSize(1);
+ }
+
+ /**
+ * Difference between sync and async requests is that
+ * In async requests you need {@link SimpleRequestBuilder#setAuthority(URIAuthority)} explicitly
+ * And in this case exception will be thrown from {@link org.apache.hc.client5.http.impl.async.AsyncProtocolExec#execute}
+ *
+ * SimpleHttpRequest req = SimpleRequestBuilder.get().setPath(path)
+ * .setScheme("http")
+ * .setAuthority(new URIAuthority(uri.getUserInfo(), uri.getHost(), uri.getPort()))
+ * .build();
+ */
+ @Override
+ public boolean isTestHttpCallWithUserInfoEnabled() {
+ return true;
+ }
+}
diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java
new file mode 100644
index 0000000000..7ed5a2163c
--- /dev/null
+++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.httpclient.v5;
+
+
+import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest;
+import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+
+public class ApacheHttpClientInstrumentationTest extends AbstractHttpClientInstrumentationTest {
+
+ private static CloseableHttpClient client;
+
+ @BeforeClass
+ public static void setUp() {
+ client = HttpClients.createDefault();
+ }
+
+ @AfterClass
+ public static void close() throws IOException {
+ client.close();
+ }
+
+ /**
+ * RFC 7230: treat presence of userinfo in authority component in request URI as an HTTP protocol violation.
+ *
+ * Uses {@link org.apache.hc.core5.http.message.BasicHttpRequest#setUri} to fill {@link org.apache.hc.core5.net.URIAuthority}
+ *
+ * Assertions on authority in {@link org.apache.hc.client5.http.impl.classic.ProtocolExec#execute}
+ */
+ @Override
+ public boolean isTestHttpCallWithUserInfoEnabled() {
+ return false;
+ }
+
+ @Override
+ protected void performGet(String path) throws Exception {
+ HttpClientResponseHandler responseHandler = response -> {
+ int status = response.getCode();
+ if (status >= 200 && status < 300) {
+ HttpEntity entity = response.getEntity();
+ String res = entity != null ? EntityUtils.toString(entity) : null;
+ return res;
+ } else {
+ throw new ClientProtocolException("Unexpected response status: " + status);
+ }
+ };
+ String response = client.execute(new HttpGet(path), responseHandler);
+ }
+}
diff --git a/apm-agent-plugins/apm-apache-httpclient/pom.xml b/apm-agent-plugins/apm-apache-httpclient/pom.xml
index 8fccd3bf88..003a279727 100644
--- a/apm-agent-plugins/apm-apache-httpclient/pom.xml
+++ b/apm-agent-plugins/apm-apache-httpclient/pom.xml
@@ -18,7 +18,9 @@
apm-apache-httpclient3-plugin
+ apm-apache-httpclient-common
apm-apache-httpclient4-plugin
+ apm-apache-httpclient5-plugin
diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
index 816a10a8df..4f53239439 100644
--- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
+++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
@@ -14,6 +14,7 @@
${project.basedir}/../../..
true
+ 13.0
@@ -70,6 +71,31 @@
${project.version}
test