Skip to content

Commit 21d0696

Browse files
committed
Refine solution for 21de09
The following was reported after the change and is related to it: reactor/reactor-netty#1170. An HTTP HEAD with the body not consumed. Connection is disposed and closed leading to subsequent request to fail. Adding toBodilessEntity() helps. This change does not close the connection but rather drains the body which does not impact subsequent re-use of the connection. This however may compete with a late subscriber actually attempting to read the response. At that point there is little choice but to raise an ISE with a more specific description. See gh-25216
1 parent 6419b18 commit 21d0696

File tree

2 files changed

+42
-24
lines changed

2 files changed

+42
-24
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
115115
.next()
116116
.doOnCancel(() -> {
117117
ReactorClientHttpResponse response = responseRef.get();
118-
if (response != null && response.bodyNotSubscribed()) {
119-
response.getConnection().dispose();
118+
if (response != null) {
119+
response.releaseAfterCancel(method);
120120
}
121121
});
122122
}

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

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.function.BiFunction;
2222

2323
import io.netty.buffer.ByteBufAllocator;
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
2426
import reactor.core.publisher.Flux;
2527
import reactor.netty.Connection;
2628
import reactor.netty.NettyInbound;
@@ -29,10 +31,10 @@
2931
import org.springframework.core.io.buffer.DataBuffer;
3032
import org.springframework.core.io.buffer.NettyDataBufferFactory;
3133
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpMethod;
3235
import org.springframework.http.HttpStatus;
3336
import org.springframework.http.ResponseCookie;
3437
import org.springframework.lang.Nullable;
35-
import org.springframework.util.Assert;
3638
import org.springframework.util.CollectionUtils;
3739
import org.springframework.util.LinkedMultiValueMap;
3840
import org.springframework.util.MultiValueMap;
@@ -46,16 +48,18 @@
4648
*/
4749
class ReactorClientHttpResponse implements ClientHttpResponse {
4850

51+
private static final Log logger = LogFactory.getLog(ReactorClientHttpResponse.class);
52+
4953
private final NettyDataBufferFactory bufferFactory;
5054

5155
private final HttpClientResponse response;
5256

5357
private final NettyInbound inbound;
5458

5559
@Nullable
56-
private final Connection connection;
60+
private final String logPrefix;
5761

58-
// 0 - not subscribed, 1 - subscribed, 2 - cancelled
62+
// 0 - not subscribed, 1 - subscribed, 2 - cancelled, 3 - cancelled via connector (before subscribe)
5963
private final AtomicInteger state = new AtomicInteger(0);
6064

6165

@@ -68,7 +72,7 @@ public ReactorClientHttpResponse(HttpClientResponse response, Connection connect
6872
this.response = response;
6973
this.inbound = connection.inbound();
7074
this.bufferFactory = new NettyDataBufferFactory(connection.outbound().alloc());
71-
this.connection = connection;
75+
this.logPrefix = (logger.isDebugEnabled() ? "[" + connection.channel().id().asShortText() + "] " : "");
7276
}
7377

7478
/**
@@ -80,22 +84,28 @@ public ReactorClientHttpResponse(HttpClientResponse response, NettyInbound inbou
8084
this.response = response;
8185
this.inbound = inbound;
8286
this.bufferFactory = new NettyDataBufferFactory(alloc);
83-
this.connection = null;
87+
this.logPrefix = "";
8488
}
8589

8690

8791
@Override
8892
public Flux<DataBuffer> getBody() {
8993
return this.inbound.receive()
9094
.doOnSubscribe(s -> {
91-
if (!this.state.compareAndSet(0, 1)) {
92-
// https://github.com/reactor/reactor-netty/issues/503
93-
// FluxReceive rejects multiple subscribers, but not after a cancel().
94-
// Subsequent subscribers after cancel() will not be rejected, but will hang instead.
95-
// So we need to reject once in cancelled state.
96-
if (this.state.get() == 2) {
97-
throw new IllegalStateException("The client response body can only be consumed once.");
98-
}
95+
if (this.state.compareAndSet(0, 1)) {
96+
return;
97+
}
98+
// https://github.com/reactor/reactor-netty/issues/503
99+
// FluxReceive rejects multiple subscribers, but not after a cancel().
100+
// Subsequent subscribers after cancel() will not be rejected, but will hang instead.
101+
// So we need to reject once in cancelled state.
102+
if (this.state.get() == 2) {
103+
throw new IllegalStateException(
104+
"The client response body can only be consumed once.");
105+
}
106+
else if (this.state.get() == 3) {
107+
throw new IllegalStateException(
108+
"The client response body has been released already due to cancellation.");
99109
}
100110
})
101111
.doOnCancel(() -> this.state.compareAndSet(1, 2))
@@ -127,6 +137,7 @@ public MultiValueMap<String, ResponseCookie> getCookies() {
127137
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
128138
this.response.cookies().values().stream().flatMap(Collection::stream)
129139
.forEach(c ->
140+
130141
result.add(c.name(), ResponseCookie.fromClientResponse(c.name(), c.value())
131142
.domain(c.domain())
132143
.path(c.path())
@@ -138,18 +149,25 @@ public MultiValueMap<String, ResponseCookie> getCookies() {
138149
}
139150

140151
/**
141-
* For use by {@link ReactorClientHttpConnector}.
152+
* Called by {@link ReactorClientHttpConnector} when a cancellation is detected
153+
* but the content has not been subscribed to. If the subscription never
154+
* materializes then the content will remain not drained. Or it could still
155+
* materialize if the cancellation happened very early, or the response
156+
* reading was delayed for some reason.
142157
*/
143-
boolean bodyNotSubscribed() {
144-
return this.state.get() == 0;
158+
void releaseAfterCancel(HttpMethod method) {
159+
if (mayHaveBody(method) && this.state.compareAndSet(0, 3)) {
160+
if (logger.isDebugEnabled()) {
161+
logger.debug(this.logPrefix + "Releasing body, not yet subscribed.");
162+
}
163+
this.inbound.receive().doOnNext(byteBuf -> {}).subscribe(byteBuf -> {}, ex -> {});
164+
}
145165
}
146166

147-
/**
148-
* For use by {@link ReactorClientHttpConnector}.
149-
*/
150-
Connection getConnection() {
151-
Assert.notNull(this.connection, "Constructor with connection wasn't used");
152-
return this.connection;
167+
private boolean mayHaveBody(HttpMethod method) {
168+
int code = this.getRawStatusCode();
169+
return !((code >= 100 && code < 200) || code == 204 || code == 205 ||
170+
method.equals(HttpMethod.HEAD) || getHeaders().getContentLength() == 0);
153171
}
154172

155173
@Override

0 commit comments

Comments
 (0)