Skip to content

Commit

Permalink
ApacheHttpClientBlockingChannel uses URL to build request
Browse files Browse the repository at this point in the history
  • Loading branch information
schlosna committed Nov 26, 2024
1 parent 14b1908 commit 58441ca
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.palantir.dialogue.hc5;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
Expand All @@ -33,7 +34,9 @@
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.SafeLoggable;
import com.palantir.logsafe.UnsafeArg;
import com.palantir.logsafe.exceptions.SafeExceptions;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.exceptions.SafeRuntimeException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
Expand All @@ -45,6 +48,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -62,11 +66,13 @@
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.function.Supplier;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.net.URIAuthority;

final class ApacheHttpClientBlockingChannel implements BlockingChannel {
private static final SafeLogger log = SafeLoggerFactory.get(ApacheHttpClientBlockingChannel.class);
Expand Down Expand Up @@ -98,31 +104,12 @@ final class ApacheHttpClientBlockingChannel implements BlockingChannel {

@Override
public Response execute(Endpoint endpoint, Request request) throws IOException {
// Create base request given the URL
URL target = baseUrl.render(endpoint, request);
ClassicRequestBuilder builder =
ClassicRequestBuilder.create(endpoint.httpMethod().name()).setUri(target.toString());

// Fill headers
request.headerParams().forEach(builder::addHeader);

if (request.body().isPresent()) {
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.GET, "GET endpoints must not have a request body");
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.HEAD, "HEAD endpoints must not have a request body");
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.OPTIONS, "OPTIONS endpoints must not have a request body");
RequestBody body = request.body().get();
setBody(builder, body);
} else if (requiresEmptyBody(endpoint)) {
builder.setEntity(EmptyHttpEntity.INSTANCE);
}
long startTime = System.nanoTime();
try {
ClassicHttpRequest httpRequest = createRequest(baseUrl, endpoint, request);
HttpClientContext context = HttpClientContext.create();
resolvedHost.ifPresent(inetAddress -> DialogueRoutePlanner.set(context, inetAddress));
CloseableHttpResponse httpClientResponse = client.apacheClient().execute(builder.build(), context);
CloseableHttpResponse httpClientResponse = client.apacheClient().execute(httpRequest, context);
// Defensively ensure that resources are closed if failures occur within this block,
// for example HttpClientResponse allocation may throw an OutOfMemoryError.
boolean close = true;
Expand Down Expand Up @@ -167,6 +154,42 @@ public Response execute(Endpoint endpoint, Request request) throws IOException {
}
}

@VisibleForTesting
static ClassicHttpRequest createRequest(BaseUrl baseUrl, Endpoint endpoint, Request request) {
// Create base request given the URL
URL target = baseUrl.render(endpoint, request);
ClassicRequestBuilder builder = ClassicRequestBuilder.create(
endpoint.httpMethod().name())
.setScheme(target.getProtocol())
.setAuthority(parseAuthority(target))
.setPath(target.getFile());

// Fill headers
request.headerParams().forEach(builder::addHeader);

if (request.body().isPresent()) {
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.GET, "GET endpoints must not have a request body");
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.HEAD, "HEAD endpoints must not have a request body");
Preconditions.checkArgument(
endpoint.httpMethod() != HttpMethod.OPTIONS, "OPTIONS endpoints must not have a request body");
setBody(builder, request.body().get());
} else if (requiresEmptyBody(endpoint)) {
builder.setEntity(EmptyHttpEntity.INSTANCE);
}
return builder.build();
}

@VisibleForTesting
static URIAuthority parseAuthority(URL url) {
try {
return URIAuthority.create(url.getAuthority());
} catch (URISyntaxException e) {
throw new SafeIllegalArgumentException("Invalid URI authority", e, UnsafeArg.of("url", url));
}
}

private Arg<?>[] failureDiagnosticArgs(Endpoint endpoint, Request request, long startTimeNanos) {
long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos);
return new Arg<?>[] {
Expand Down Expand Up @@ -315,7 +338,7 @@ public int code() {
public ListMultimap<String, String> headers() {
if (headers == null) {
ListMultimap<String, String> tmpHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
.arrayListValues()
.arrayListValues(1)
.build();
Iterator<Header> headerIterator = response.headerIterator();
while (headerIterator.hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.dialogue.hc5;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.collect.ListMultimap;
import com.palantir.dialogue.Endpoint;
import com.palantir.dialogue.HttpMethod;
import com.palantir.dialogue.PathTemplate;
import com.palantir.dialogue.Request;
import com.palantir.dialogue.UrlBuilder;
import com.palantir.dialogue.core.BaseUrl;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

class ApacheHttpClientBlockingChannelTest {

@ParameterizedTest
@ValueSource(strings = {"GET", "PUT", "POST"})
void createRequest(String method) throws Exception {

BaseUrl baseUrl = BaseUrl.of(new URL("https://www.example.com/foo/api"));
Endpoint endpoint = new Endpoint() {
private final PathTemplate pathTemplate = PathTemplate.builder()
.fixed("fixed")
.variable("endpoint")
.variable("index")
.build();

@Override
public HttpMethod httpMethod() {
return HttpMethod.valueOf(method);
}

@Override
public String serviceName() {
return "testService";
}

@Override
public String endpointName() {
return "testEndpoint";
}

@Override
public String version() {
return "1.2.3";
}

@Override
public void renderPath(ListMultimap<String, String> params, UrlBuilder url) {
pathTemplate.fill(params, url);
}
};

Request request = Request.builder()
.pathParams(Map.of("fixed", "bar", "endpoint", "baz", "index", "42"))
.putQueryParams("q", "1")
.putQueryParams("test", "true")
.build();

ClassicHttpRequest httpRequest = ApacheHttpClientBlockingChannel.createRequest(baseUrl, endpoint, request);
assertThat(httpRequest.getMethod()).isEqualTo(method);
assertThat(httpRequest.getPath())
.isEqualTo(httpRequest.getRequestUri())
.isEqualTo("/foo/api/fixed/baz/42?q=1&test=true");
assertThat(httpRequest.getUri())
.isEqualTo(URI.create("https://www.example.com/foo/api/fixed/baz/42?q=1&test=true"));
}

@ParameterizedTest
@CsvSource({
"http://example.com, example.com, -1,",
"https://www.example.com, www.example.com, -1,",
"https://www.example.com:443, www.example.com, 443,",
"https://www.example.com/path/to/foo/bar, www.example.com, -1,",
"https://www.example.com/path/to/foo/bar?baz=quux&hello=world#hash-octothorpe, www.example.com, -1,",
"https://user@www.example.com:8443/path/to/foo/bar?baz=quux&hello=world#hash-octothorpe ,"
+ " www.example.com, 8443, user",
})
void parseAuthority(String input, String expectedHost, int expectedPort, String expectedUserInfo) throws Exception {
URL url = new URL(input);
URI uri = URI.create(input);
assertThat(ApacheHttpClientBlockingChannel.parseAuthority(url))
.isEqualTo(ApacheHttpClientBlockingChannel.parseAuthority(uri.toURL()))
.isEqualTo(ApacheHttpClientBlockingChannel.parseAuthority(new URL(uri.toString())))
.satisfies(authority -> {
assertThat(authority.getHostName())
.isEqualTo(expectedHost)
.isEqualTo(uri.getHost())
.isEqualTo(url.getHost());
assertThat(authority.getPort())
.isEqualTo(expectedPort)
.isEqualTo(uri.getPort())
.isEqualTo(url.getPort());
assertThat(authority.getUserInfo())
.isEqualTo(expectedUserInfo)
.isEqualTo(uri.getUserInfo())
.isEqualTo(url.getUserInfo());
});
}
}

0 comments on commit 58441ca

Please sign in to comment.