Skip to content

Commit e8e722f

Browse files
committed
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
1 parent 292a3a4 commit e8e722f

File tree

2 files changed

+99
-66
lines changed

2 files changed

+99
-66
lines changed

spring-web/src/main/java/org/springframework/http/client/InterceptingClientHttpRequest.java

+56-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@
1919
import java.io.IOException;
2020
import java.io.OutputStream;
2121
import java.net.URI;
22-
import java.util.Iterator;
2322
import java.util.List;
23+
import java.util.ListIterator;
2424

2525
import org.springframework.http.HttpHeaders;
2626
import org.springframework.http.HttpMethod;
@@ -33,6 +33,7 @@
3333
* ClientHttpRequestInterceptors}.
3434
*
3535
* @author Arjen Poutsma
36+
* @author Brian Clozel
3637
* @since 3.1
3738
*/
3839
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
@@ -68,54 +69,71 @@ public URI getURI() {
6869

6970
@Override
7071
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
71-
InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
72+
ClientHttpRequestExecution requestExecution = new DelegatingRequestExecution(this.requestFactory);
73+
ListIterator<ClientHttpRequestInterceptor> iterator = this.interceptors.listIterator(this.interceptors.size());
74+
while (iterator.hasPrevious()) {
75+
ClientHttpRequestInterceptor interceptor = iterator.previous();
76+
requestExecution = new InterceptingRequestExecution(interceptor, requestExecution);
77+
}
7278
return requestExecution.execute(this, bufferedOutput);
7379
}
7480

7581

76-
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
82+
private static class InterceptingRequestExecution implements ClientHttpRequestExecution {
83+
84+
private final ClientHttpRequestInterceptor interceptor;
7785

78-
private final Iterator<ClientHttpRequestInterceptor> iterator;
86+
private final ClientHttpRequestExecution nextExecution;
7987

80-
public InterceptingRequestExecution() {
81-
this.iterator = interceptors.iterator();
88+
public InterceptingRequestExecution(ClientHttpRequestInterceptor interceptor, ClientHttpRequestExecution nextExecution) {
89+
this.interceptor = interceptor;
90+
this.nextExecution = nextExecution;
8291
}
8392

8493
@Override
8594
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
86-
if (this.iterator.hasNext()) {
87-
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
88-
return nextInterceptor.intercept(request, body, this);
89-
}
90-
else {
91-
HttpMethod method = request.getMethod();
92-
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
93-
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
94-
request.getAttributes().forEach((key, value) -> delegate.getAttributes().put(key, value));
95-
if (body.length > 0) {
96-
long contentLength = delegate.getHeaders().getContentLength();
97-
if (contentLength > -1 && contentLength != body.length) {
98-
delegate.getHeaders().setContentLength(body.length);
99-
}
100-
if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) {
101-
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
102-
@Override
103-
public void writeTo(OutputStream outputStream) throws IOException {
104-
StreamUtils.copy(body, outputStream);
105-
}
106-
107-
@Override
108-
public boolean repeatable() {
109-
return true;
110-
}
111-
});
112-
}
113-
else {
114-
StreamUtils.copy(body, delegate.getBody());
115-
}
95+
return this.interceptor.intercept(request, body, this.nextExecution);
96+
}
97+
98+
}
99+
100+
private static class DelegatingRequestExecution implements ClientHttpRequestExecution {
101+
102+
private final ClientHttpRequestFactory requestFactory;
103+
104+
public DelegatingRequestExecution(ClientHttpRequestFactory requestFactory) {
105+
this.requestFactory = requestFactory;
106+
}
107+
108+
@Override
109+
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
110+
HttpMethod method = request.getMethod();
111+
ClientHttpRequest delegate = this.requestFactory.createRequest(request.getURI(), method);
112+
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
113+
request.getAttributes().forEach((key, value) -> delegate.getAttributes().put(key, value));
114+
if (body.length > 0) {
115+
long contentLength = delegate.getHeaders().getContentLength();
116+
if (contentLength > -1 && contentLength != body.length) {
117+
delegate.getHeaders().setContentLength(body.length);
118+
}
119+
if (delegate instanceof StreamingHttpOutputMessage streamingOutputMessage) {
120+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
121+
@Override
122+
public void writeTo(OutputStream outputStream) throws IOException {
123+
StreamUtils.copy(body, outputStream);
124+
}
125+
126+
@Override
127+
public boolean repeatable() {
128+
return true;
129+
}
130+
});
131+
}
132+
else {
133+
StreamUtils.copy(body, delegate.getBody());
116134
}
117-
return delegate.execute();
118135
}
136+
return delegate.execute();
119137
}
120138
}
121139

spring-web/src/test/java/org/springframework/http/client/InterceptingClientHttpRequestFactoryTests.java

+43-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,8 +35,10 @@
3535
import static org.assertj.core.api.Assertions.assertThat;
3636

3737
/**
38+
* Tests for {@link InterceptingClientHttpRequestFactory}
3839
* @author Arjen Poutsma
3940
* @author Juergen Hoeller
41+
* @author Brian Clozel
4042
*/
4143
class InterceptingClientHttpRequestFactoryTests {
4244

@@ -54,7 +56,7 @@ void beforeEach() {
5456
}
5557

5658
@Test
57-
void basic() throws Exception {
59+
void shouldInvokeInterceptors() throws Exception {
5860
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
5961
interceptors.add(new NoOpInterceptor());
6062
interceptors.add(new NoOpInterceptor());
@@ -64,31 +66,30 @@ void basic() throws Exception {
6466
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
6567
ClientHttpResponse response = request.execute();
6668

67-
assertThat(((NoOpInterceptor) interceptors.get(0)).invoked).isTrue();
68-
assertThat(((NoOpInterceptor) interceptors.get(1)).invoked).isTrue();
69-
assertThat(((NoOpInterceptor) interceptors.get(2)).invoked).isTrue();
69+
assertThat(((NoOpInterceptor) interceptors.get(0)).invocationCount).isEqualTo(1);
70+
assertThat(((NoOpInterceptor) interceptors.get(1)).invocationCount).isEqualTo(1);
71+
assertThat(((NoOpInterceptor) interceptors.get(2)).invocationCount).isEqualTo(1);
7072
assertThat(requestMock.isExecuted()).isTrue();
7173
assertThat(response).isSameAs(responseMock);
7274
}
7375

7476
@Test
75-
void noExecution() throws Exception {
77+
void shouldSkipIntercetor() throws Exception {
7678
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
7779
interceptors.add((request, body, execution) -> responseMock);
78-
7980
interceptors.add(new NoOpInterceptor());
8081
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, interceptors);
8182

8283
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
8384
ClientHttpResponse response = request.execute();
8485

85-
assertThat(((NoOpInterceptor) interceptors.get(1)).invoked).isFalse();
86+
assertThat(((NoOpInterceptor) interceptors.get(1)).invocationCount).isZero();
8687
assertThat(requestMock.isExecuted()).isFalse();
8788
assertThat(response).isSameAs(responseMock);
8889
}
8990

9091
@Test
91-
void changeHeaders() throws Exception {
92+
void interceptorShouldUpdateRequestHeader() throws Exception {
9293
final String headerName = "Foo";
9394
final String headerValue = "Bar";
9495
final String otherValue = "Baz";
@@ -98,7 +99,6 @@ void changeHeaders() throws Exception {
9899
wrapper.getHeaders().add(headerName, otherValue);
99100
return execution.execute(wrapper, body);
100101
};
101-
102102
requestMock = new MockClientHttpRequest() {
103103
@Override
104104
protected ClientHttpResponse executeInternal() {
@@ -110,39 +110,34 @@ protected ClientHttpResponse executeInternal() {
110110
requestMock.getHeaders().add(headerName, headerValue);
111111

112112
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, Collections.singletonList(interceptor));
113-
114113
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
115114
request.execute();
116115
}
117116

118117
@Test
119-
void changeAttribute() throws Exception {
118+
void interceptorShouldUpdateRequestAttribute() throws Exception {
120119
final String attrName = "Foo";
121120
final String attrValue = "Bar";
122121

123122
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
124-
System.out.println("interceptor");
125123
request.getAttributes().put(attrName, attrValue);
126124
return execution.execute(request, body);
127125
};
128-
129126
requestMock = new MockClientHttpRequest() {
130127
@Override
131128
protected ClientHttpResponse executeInternal() {
132-
System.out.println("execute");
133129
assertThat(getAttributes()).containsEntry(attrName, attrValue);
134130
return responseMock;
135131
}
136132
};
137-
138133
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, Collections.singletonList(interceptor));
139134

140135
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
141136
request.execute();
142137
}
143138

144139
@Test
145-
void changeURI() throws Exception {
140+
void interceptorShouldUpdateRequestURI() throws Exception {
146141
final URI changedUri = URI.create("https://example.com/2");
147142

148143
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> execution.execute(new HttpRequestWrapper(request) {
@@ -152,23 +147,21 @@ public URI getURI() {
152147
}
153148

154149
}, body);
155-
156150
requestFactoryMock = new RequestFactoryMock() {
157151
@Override
158152
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
159153
assertThat(uri).isEqualTo(changedUri);
160154
return super.createRequest(uri, httpMethod);
161155
}
162156
};
163-
164157
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, Collections.singletonList(interceptor));
165158

166159
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
167160
request.execute();
168161
}
169162

170163
@Test
171-
void changeMethod() throws Exception {
164+
void interceptorShouldUpdateRequestMethod() throws Exception {
172165
final HttpMethod changedMethod = HttpMethod.POST;
173166

174167
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> execution.execute(new HttpRequestWrapper(request) {
@@ -178,44 +171,66 @@ public HttpMethod getMethod() {
178171
}
179172

180173
}, body);
181-
182174
requestFactoryMock = new RequestFactoryMock() {
183175
@Override
184176
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
185177
assertThat(httpMethod).isEqualTo(changedMethod);
186178
return super.createRequest(uri, httpMethod);
187179
}
188180
};
189-
190181
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, Collections.singletonList(interceptor));
191182

192183
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
193184
request.execute();
194185
}
195186

196187
@Test
197-
void changeBody() throws Exception {
188+
void interceptorShouldUpdateRequestBody() throws Exception {
198189
final byte[] changedBody = "Foo".getBytes();
199-
200190
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> execution.execute(request, changedBody);
201-
202191
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, Collections.singletonList(interceptor));
203-
204192
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
205193
request.execute();
194+
206195
assertThat(Arrays.equals(changedBody, requestMock.getBodyAsBytes())).isTrue();
207196
assertThat(requestMock.getHeaders().getContentLength()).isEqualTo(changedBody.length);
208197
}
209198

199+
@Test
200+
void interceptorShouldAlwaysExecuteNextInterceptor() throws Exception {
201+
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
202+
interceptors.add(new MultipleExecutionInterceptor());
203+
interceptors.add(new NoOpInterceptor());
204+
requestFactory = new InterceptingClientHttpRequestFactory(requestFactoryMock, interceptors);
205+
206+
ClientHttpRequest request = requestFactory.createRequest(URI.create("https://example.com"), HttpMethod.GET);
207+
ClientHttpResponse response = request.execute();
208+
209+
assertThat(((NoOpInterceptor) interceptors.get(1)).invocationCount).isEqualTo(2);
210+
assertThat(requestMock.isExecuted()).isTrue();
211+
assertThat(response).isSameAs(responseMock);
212+
}
213+
210214

211215
private static class NoOpInterceptor implements ClientHttpRequestInterceptor {
212216

213-
private boolean invoked = false;
217+
private int invocationCount = 0;
218+
219+
@Override
220+
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
221+
throws IOException {
222+
invocationCount++;
223+
return execution.execute(request, body);
224+
}
225+
}
226+
227+
private static class MultipleExecutionInterceptor implements ClientHttpRequestInterceptor {
214228

215229
@Override
216230
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
217231
throws IOException {
218-
invoked = true;
232+
// execute another request first
233+
execution.execute(new MockClientHttpRequest(), body);
219234
return execution.execute(request, body);
220235
}
221236
}

0 commit comments

Comments
 (0)