Skip to content

Commit 21de098

Browse files
committed
Dispose on cancel if response not subscribed
Closes gh-25216
1 parent 9b615ed commit 21de098

File tree

2 files changed

+67
-23
lines changed

2 files changed

+67
-23
lines changed

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -17,15 +17,13 @@
1717
package org.springframework.http.client.reactive;
1818

1919
import java.net.URI;
20+
import java.util.concurrent.atomic.AtomicReference;
2021
import java.util.function.Function;
2122

22-
import io.netty.buffer.ByteBufAllocator;
2323
import reactor.core.publisher.Mono;
24-
import reactor.netty.NettyInbound;
2524
import reactor.netty.NettyOutbound;
2625
import reactor.netty.http.client.HttpClient;
2726
import reactor.netty.http.client.HttpClientRequest;
28-
import reactor.netty.http.client.HttpClientResponse;
2927
import reactor.netty.resources.ConnectionProvider;
3028
import reactor.netty.resources.LoopResources;
3129

@@ -104,12 +102,23 @@ public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
104102
return Mono.error(new IllegalArgumentException("URI is not absolute: " + uri));
105103
}
106104

105+
AtomicReference<ReactorClientHttpResponse> responseRef = new AtomicReference<>();
106+
107107
return this.httpClient
108108
.request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()))
109109
.uri(uri.toString())
110110
.send((request, outbound) -> requestCallback.apply(adaptRequest(method, uri, request, outbound)))
111-
.responseConnection((res, con) -> Mono.just(adaptResponse(res, con.inbound(), con.outbound().alloc())))
112-
.next();
111+
.responseConnection((response, connection) -> {
112+
responseRef.set(new ReactorClientHttpResponse(response, connection));
113+
return Mono.just((ClientHttpResponse) responseRef.get());
114+
})
115+
.next()
116+
.doOnCancel(() -> {
117+
ReactorClientHttpResponse response = responseRef.get();
118+
if (response != null && response.bodyNotSubscribed()) {
119+
response.getConnection().dispose();
120+
}
121+
});
113122
}
114123

115124
private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request,
@@ -118,10 +127,4 @@ private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpCl
118127
return new ReactorClientHttpRequest(method, uri, request, nettyOutbound);
119128
}
120129

121-
private ClientHttpResponse adaptResponse(HttpClientResponse response, NettyInbound nettyInbound,
122-
ByteBufAllocator allocator) {
123-
124-
return new ReactorClientHttpResponse(response, nettyInbound, allocator);
125-
}
126-
127130
}

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

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
package org.springframework.http.client.reactive;
1818

1919
import java.util.Collection;
20-
import java.util.concurrent.atomic.AtomicBoolean;
20+
import java.util.concurrent.atomic.AtomicInteger;
21+
import java.util.function.BiFunction;
2122

2223
import io.netty.buffer.ByteBufAllocator;
2324
import reactor.core.publisher.Flux;
25+
import reactor.netty.Connection;
2426
import reactor.netty.NettyInbound;
2527
import reactor.netty.http.client.HttpClientResponse;
2628

@@ -29,6 +31,8 @@
2931
import org.springframework.http.HttpHeaders;
3032
import org.springframework.http.HttpStatus;
3133
import org.springframework.http.ResponseCookie;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.util.Assert;
3236
import org.springframework.util.CollectionUtils;
3337
import org.springframework.util.LinkedMultiValueMap;
3438
import org.springframework.util.MultiValueMap;
@@ -48,31 +52,53 @@ class ReactorClientHttpResponse implements ClientHttpResponse {
4852

4953
private final NettyInbound inbound;
5054

51-
private final AtomicBoolean rejectSubscribers = new AtomicBoolean();
55+
@Nullable
56+
private final Connection connection;
5257

58+
// 0 - not subscribed, 1 - subscribed, 2 - cancelled
59+
private final AtomicInteger state = new AtomicInteger(0);
5360

61+
62+
/**
63+
* Constructor that matches the inputs from
64+
* {@link reactor.netty.http.client.HttpClient.ResponseReceiver#responseConnection(BiFunction)}.
65+
* @since 5.3
66+
*/
67+
public ReactorClientHttpResponse(HttpClientResponse response, Connection connection) {
68+
this.response = response;
69+
this.inbound = connection.inbound();
70+
this.bufferFactory = new NettyDataBufferFactory(connection.outbound().alloc());
71+
this.connection = connection;
72+
}
73+
74+
/**
75+
* Constructor with inputs extracted from a {@link Connection}.
76+
* @deprecated as of 5.2.8
77+
*/
78+
@Deprecated
5479
public ReactorClientHttpResponse(HttpClientResponse response, NettyInbound inbound, ByteBufAllocator alloc) {
5580
this.response = response;
5681
this.inbound = inbound;
5782
this.bufferFactory = new NettyDataBufferFactory(alloc);
83+
this.connection = null;
5884
}
5985

6086

6187
@Override
6288
public Flux<DataBuffer> getBody() {
6389
return this.inbound.receive()
6490
.doOnSubscribe(s -> {
65-
if (this.rejectSubscribers.get()) {
66-
throw new IllegalStateException("The client response body can only be consumed once.");
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+
}
6799
}
68100
})
69-
.doOnCancel(() ->
70-
// https://github.com/reactor/reactor-netty/issues/503
71-
// FluxReceive rejects multiple subscribers, but not after a cancel().
72-
// Subsequent subscribers after cancel() will not be rejected, but will hang instead.
73-
// So we need to intercept and reject them in that case.
74-
this.rejectSubscribers.set(true)
75-
)
101+
.doOnCancel(() -> this.state.compareAndSet(1, 2))
76102
.map(byteBuf -> {
77103
byteBuf.retain();
78104
return this.bufferFactory.wrap(byteBuf);
@@ -111,6 +137,21 @@ public MultiValueMap<String, ResponseCookie> getCookies() {
111137
return CollectionUtils.unmodifiableMultiValueMap(result);
112138
}
113139

140+
/**
141+
* For use by {@link ReactorClientHttpConnector}.
142+
*/
143+
boolean bodyNotSubscribed() {
144+
return this.state.get() == 0;
145+
}
146+
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;
153+
}
154+
114155
@Override
115156
public String toString() {
116157
return "ReactorClientHttpResponse{" +

0 commit comments

Comments
 (0)