Skip to content

Instantly share code, notes, and snippets.

@robtimus
Created April 7, 2025 19:31
Show Gist options
  • Save robtimus/106cf5229aca589d09aee6523b1b3d85 to your computer and use it in GitHub Desktop.
Save robtimus/106cf5229aca589d09aee6523b1b3d85 to your computer and use it in GitHub Desktop.
RESTEasy client engine implementation based on java.net.http.HttpClient
/*
* HttpClientEngine.java
* Copyright 2025 Rob Spoor
*
* 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.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.InvocationCallback;
import org.jboss.resteasy.client.jaxrs.engines.AsyncClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.i18n.Messages;
import org.jboss.resteasy.client.jaxrs.internal.ClientConfiguration;
import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponse;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
@SuppressWarnings({ "javadoc", "nls" })
public class HttpClientEngine implements AsyncClientHttpEngine {
private final HttpClient httpClient;
private Version version;
private boolean expectContinue;
private Duration timeout;
public HttpClientEngine(HttpClient httpClient) {
this.httpClient = Objects.requireNonNull(httpClient);
}
@Override
public SSLContext getSslContext() {
return httpClient.sslContext();
}
@Override
public HostnameVerifier getHostnameVerifier() {
return null;
}
@Override
public void close() {
// HttpClient doesn't provide any cleanup methods
}
@Override
public ClientResponse invoke(Invocation request) {
return invoke((ClientInvocation) request);
}
private ClientResponse invoke(ClientInvocation request) {
HttpRequest httpRequest = toHttpRequest(request);
try {
HttpResponse<InputStream> httpResponse = httpClient.send(httpRequest, BodyHandlers.ofInputStream());
return HttpResponseClientResponse.create(httpResponse, request.getClientConfiguration());
} catch (IOException e) {
throw new ProcessingException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ProcessingException(e);
}
}
@Override
public <T> CompletableFuture<T> submit(ClientInvocation request, boolean buffered, InvocationCallback<T> callback, ResultExtractor<T> extractor) {
HttpRequest httpRequest = toHttpRequest(request);
CompletableFuture<T> result = httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream())
.thenApply(response -> HttpResponseClientResponse.create(response, request.getClientConfiguration()))
.thenApply(extractor::extractResult);
if (callback != null) {
result.whenComplete((r, t) -> {
if (r != null) {
callback.completed(r);
} else if (t != null) {
callback.failed(t);
}
});
}
return result;
}
@Override
public <T> CompletableFuture<T> submit(ClientInvocation request, boolean buffered, ResultExtractor<T> extractor,
ExecutorService executorService) {
return submit(request, buffered, null, extractor);
}
private HttpRequest toHttpRequest(ClientInvocation request) {
HttpRequest.Builder builder = HttpRequest.newBuilder(request.getUri());
for (Map.Entry<String, List<String>> entry : request.getHeaders().asMap().entrySet()) {
String name = entry.getKey();
for (String value : entry.getValue()) {
builder = builder.header(name, value);
}
}
BodyPublisher bodyPublisher = getBodyPublisher(request);
builder = builder.method(request.getMethod(), bodyPublisher);
if (version != null) {
builder = builder.version(version);
}
if (timeout != null) {
builder.timeout(timeout);
}
builder.expectContinue(expectContinue);
return builder.build();
}
@SuppressWarnings("resource")
private BodyPublisher getBodyPublisher(ClientInvocation request) {
Object entity = request.getEntity();
if (entity == null) {
return BodyPublishers.noBody();
}
if ("GET".equalsIgnoreCase(request.getMethod())) {
throw new ProcessingException(Messages.MESSAGES.getRequestCannotHaveBody());
}
if (entity instanceof InputStream inputStream) {
return BodyPublishers.ofInputStream(() -> inputStream);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
request.getDelegatingOutputStream().setDelegate(baos);
request.writeRequestBody(request.getEntityStream());
} catch (IOException e) {
throw new ProcessingException(e);
}
return BodyPublishers.ofByteArray(baos.toByteArray());
}
private static final class HttpResponseClientResponse extends ClientResponse {
private final HttpResponse<InputStream> httpResponse;
private InputStream inputStream;
private HttpResponseClientResponse(HttpResponse<InputStream> httpResponse, ClientConfiguration configuration,
RESTEasyTracingLogger tracingLogger) {
super(configuration, tracingLogger);
this.httpResponse = httpResponse;
}
@SuppressWarnings("unchecked")
private static ClientResponse create(HttpResponse<InputStream> httpResponse, ClientConfiguration configuration) {
ClientResponse clientResponse = new HttpResponseClientResponse(httpResponse, configuration, RESTEasyTracingLogger.empty());
clientResponse.setStatus(httpResponse.statusCode());
for (Map.Entry<String, List<String>> entry : httpResponse.headers().map().entrySet()) {
clientResponse.getHeaders().addAll(entry.getKey(), (List<Object>) (List<?>) entry.getValue());
}
return clientResponse;
}
@Override
protected void setInputStream(InputStream is) {
this.inputStream = is;
}
@Override
protected InputStream getInputStream() {
if (inputStream == null) {
inputStream = httpResponse.body();
}
return inputStream;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment