Update contribution

Closes gh-33090
This commit is contained in:
rstoyanchev 2024-09-25 16:01:48 +01:00
parent 883f123583
commit dea4f71846
2 changed files with 72 additions and 35 deletions

View File

@ -36,6 +36,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Flow; import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -94,38 +95,25 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@Override @Override
@SuppressWarnings("NullAway") @SuppressWarnings("NullAway")
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException { protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
HttpRequest request = buildRequest(headers, body); CompletableFuture<HttpResponse<InputStream>> responseFuture = null;
CompletableFuture<HttpResponse<InputStream>> responsefuture =
this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
try { try {
HttpRequest request = buildRequest(headers, body);
responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
if (this.timeout != null) { if (this.timeout != null) {
CompletableFuture<Void> timeoutFuture = new CompletableFuture<Void>() TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
.completeOnTimeout(null, this.timeout.toMillis(), TimeUnit.MILLISECONDS); HttpResponse<InputStream> response = responseFuture.get();
timeoutFuture.thenRun(() -> { InputStream inputStream = timeoutHandler.wrapInputStream(response);
if (!responsefuture.cancel(true) && !responsefuture.isCompletedExceptionally()) { return new JdkClientHttpResponse(response, inputStream);
try { }
responsefuture.resultNow().body().close(); else {
} catch (IOException ignored) {} HttpResponse<InputStream> response = responseFuture.get();
} return new JdkClientHttpResponse(response, response.body());
});
var response = responsefuture.get();
return new JdkClientHttpResponse(response.statusCode(), response.headers(), new FilterInputStream(response.body()) {
@Override
public void close() throws IOException {
timeoutFuture.cancel(false);
super.close();
}
});
} else {
var response = responsefuture.get();
return new JdkClientHttpResponse(response.statusCode(), response.headers(), response.body());
} }
} }
catch (InterruptedException ex) { catch (InterruptedException ex) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
responsefuture.cancel(true); responseFuture.cancel(true);
throw new IOException("Request was interrupted: " + ex.getMessage(), ex); throw new IOException("Request was interrupted: " + ex.getMessage(), ex);
} }
catch (ExecutionException ex) { catch (ExecutionException ex) {
@ -149,7 +137,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
} }
} }
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) { private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(this.uri); HttpRequest.Builder builder = HttpRequest.newBuilder().uri(this.uri);
@ -225,4 +212,52 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
} }
} }
/**
* Temporary workaround to use instead of {@link HttpRequest.Builder#timeout(Duration)}
* until <a href="https://bugs.openjdk.org/browse/JDK-8258397">JDK-8258397</a>
* is fixed. Essentially, create a future wiht a timeout handler, and use it
* to close the response.
* @see <a href="https://mail.openjdk.org/pipermail/net-dev/2021-October/016672.html">OpenJDK discussion thread</a>
*/
private static final class TimeoutHandler {
private final CompletableFuture<Void> timeoutFuture;
private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Duration timeout) {
this.timeoutFuture = new CompletableFuture<Void>()
.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS);
this.timeoutFuture.thenRun(() -> {
if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) {
return;
}
try {
future.get().body().close();
}
catch (Exception ex) {
// ignore
}
});
}
@Nullable
public InputStream wrapInputStream(HttpResponse<InputStream> response) {
InputStream body = response.body();
if (body == null) {
return body;
}
return new FilterInputStream(body) {
@Override
public void close() throws IOException {
TimeoutHandler.this.timeoutFuture.cancel(false);
super.close();
}
};
}
}
} }

View File

@ -19,6 +19,7 @@ package org.springframework.http.client;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -26,6 +27,7 @@ import java.util.Map;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -40,21 +42,21 @@ import org.springframework.util.StreamUtils;
*/ */
class JdkClientHttpResponse implements ClientHttpResponse { class JdkClientHttpResponse implements ClientHttpResponse {
private final int statusCode; private final HttpResponse<InputStream> response;
private final HttpHeaders headers; private final HttpHeaders headers;
private final InputStream body; private final InputStream body;
public JdkClientHttpResponse(int statusCode, java.net.http.HttpHeaders headers, InputStream body) { public JdkClientHttpResponse(HttpResponse<InputStream> response, @Nullable InputStream body) {
this.statusCode = statusCode; this.response = response;
this.headers = adaptHeaders(headers); this.headers = adaptHeaders(response);
this.body = body != null ? body : InputStream.nullInputStream(); this.body = (body != null ? body : InputStream.nullInputStream());
} }
private static HttpHeaders adaptHeaders(java.net.http.HttpHeaders headers) { private static HttpHeaders adaptHeaders(HttpResponse<?> response) {
Map<String, List<String>> rawHeaders = headers.map(); Map<String, List<String>> rawHeaders = response.headers().map();
Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(rawHeaders.size(), Locale.ENGLISH); Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(rawHeaders.size(), Locale.ENGLISH);
MultiValueMap<String, String> multiValueMap = CollectionUtils.toMultiValueMap(map); MultiValueMap<String, String> multiValueMap = CollectionUtils.toMultiValueMap(map);
multiValueMap.putAll(rawHeaders); multiValueMap.putAll(rawHeaders);
@ -64,7 +66,7 @@ class JdkClientHttpResponse implements ClientHttpResponse {
@Override @Override
public HttpStatusCode getStatusCode() { public HttpStatusCode getStatusCode() {
return HttpStatusCode.valueOf(statusCode); return HttpStatusCode.valueOf(this.response.statusCode());
} }
@Override @Override