Allow multiple executions of ClientHttpRequestInterceptors

Prior to this commit, an `ClientHttpRequestInterceptor` implementation
could delegate HTTP calls to the next `ClientHttpRequestExecution` only
once. Calling the execution would advance to the next interceptor in the
chain in a mutable fashion for the entire lifetime of the current
exchange.

This commit changes the implementation of `InterceptingClientHttpRequest`
so that a `ClientHttpRequestInterceptor` implementation can call
`ClientHttpRequestExecution#execute` multiple times.

This is especially useful for interceptors in case they want to issue
other HTTP requests without needing another `RestTemplate` or
`RestClient` instance provided out of band.

Closes gh-34169
This commit is contained in:
Brian Clozel
2025-01-06 19:29:09 +01:00
parent 292a3a4895
commit e8e722fb59
2 changed files with 98 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@ package org.springframework.http.client;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -33,6 +33,7 @@ import org.springframework.util.StreamUtils;
* ClientHttpRequestInterceptors}.
*
* @author Arjen Poutsma
* @author Brian Clozel
* @since 3.1
*/
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
@@ -68,54 +69,71 @@ class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
@Override
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
ClientHttpRequestExecution requestExecution = new DelegatingRequestExecution(this.requestFactory);
ListIterator<ClientHttpRequestInterceptor> iterator = this.interceptors.listIterator(this.interceptors.size());
while (iterator.hasPrevious()) {
ClientHttpRequestInterceptor interceptor = iterator.previous();
requestExecution = new InterceptingRequestExecution(interceptor, requestExecution);
}
return requestExecution.execute(this, bufferedOutput);
}
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private static class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
private final ClientHttpRequestInterceptor interceptor;
public InterceptingRequestExecution() {
this.iterator = interceptors.iterator();
private final ClientHttpRequestExecution nextExecution;
public InterceptingRequestExecution(ClientHttpRequestInterceptor interceptor, ClientHttpRequestExecution nextExecution) {
this.interceptor = interceptor;
this.nextExecution = nextExecution;
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
HttpMethod method = request.getMethod();
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
request.getAttributes().forEach((key, value) -> delegate.getAttributes().put(key, value));
if (body.length > 0) {
long contentLength = delegate.getHeaders().getContentLength();
if (contentLength > -1 && contentLength != body.length) {
delegate.getHeaders().setContentLength(body.length);
}
if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(body, outputStream);
}
return this.interceptor.intercept(request, body, this.nextExecution);
}
@Override
public boolean repeatable() {
return true;
}
});
}
else {
StreamUtils.copy(body, delegate.getBody());
}
}
private static class DelegatingRequestExecution implements ClientHttpRequestExecution {
private final ClientHttpRequestFactory requestFactory;
public DelegatingRequestExecution(ClientHttpRequestFactory requestFactory) {
this.requestFactory = requestFactory;
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
HttpMethod method = request.getMethod();
ClientHttpRequest delegate = this.requestFactory.createRequest(request.getURI(), method);
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
request.getAttributes().forEach((key, value) -> delegate.getAttributes().put(key, value));
if (body.length > 0) {
long contentLength = delegate.getHeaders().getContentLength();
if (contentLength > -1 && contentLength != body.length) {
delegate.getHeaders().setContentLength(body.length);
}
if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(body, outputStream);
}
@Override
public boolean repeatable() {
return true;
}
});
}
else {
StreamUtils.copy(body, delegate.getBody());
}
return delegate.execute();
}
return delegate.execute();
}
}