Pre-check
- [x] I am sure that all the content I provide is in English.
Search before asking
- [x] I had searched in the issues and found no similar feature requirement.
Apache Dubbo Component
Java SDK (apache/dubbo)
Descriptions
When a server sends a large volume of data over a stream (e.g., gRPC or HTTP/2) and abruptly closes the stream after transmission, the client may encounter data reception issues (like #15501) due to the following reasons:
Premature Stream Closure vs. Graceful Termination: Abrupt Closure (e.g., TCP RST, RST_STREAM in HTTP/2): If the server forcibly terminates the stream (via close() or socket-level resets), pending data in OS/kernel buffers may be discarded. The client detects an unexpected stream reset, treating it as an error (e.g., ECONNRESET). Graceful Closure: The server should signal "end-of-stream" (e.g., END_STREAM flag in HTTP/2, gRPC trailing metadata) to notify the client that transmission is complete. Skipping this step causes the client to await more data indefinitely.
Flow Control and Backpressure: Protocols like HTTP/2 use flow-control windows to prevent overwhelming the receiver. If the server sends data faster than the client can process it: The client’s receive window fills, forcing the server to pause. If the server ignores this and closes the stream mid-transmission, the client may miss data still in transit or encounter protocol errors (e.g., FLOW_CONTROL_ERROR).
Data Buffering and Fragmentation: Large data is split into smaller frames/packets. If the server closes prematurely: The client may receive partial/incomplete messages (e.g., corrupted gRPC messages missing length prefixes). The client’s parser fails to deserialize fragmented data, throwing exceptions.
Protocol-Specific Constraints: gRPC: Requires a trailing metadata frame (with status code) to end the stream. Closing without this frame leaves the client in an ambiguous state (waiting for status). HTTP/2: Abrupt RST_STREAM interrupts mid-flow, signaling CANCEL or INTERNAL_ERROR. The client discards buffered data.
Resource Cleanup Timing: When the server closes immediately after sending: Network packets may be lost/reordered (especially under high load). The client’s OS/kernel buffers might not have delivered all data before the closure notification arrives.
Solution: Graceful Stream Termination To avoid client-side issues: Signal Completion Explicitly: Use protocol-specific end markers (e.g., gRPC trailing headers, HTTP/2 END_STREAM). Respect Flow Control: Ensure all data is acknowledged by the client before closing. Avoid Forceful Closure: Replace close()/RST_STREAM with a two-step close: Send all data + end-of-stream marker. Wait for client acknowledgment (if applicable). Then close the connection cleanly.
In addition to the issue #15501 mentioned above, other modules may exhibit similar vulnerabilities and require thorough investigation.
Related issues
No response
Are you willing to submit a pull request to fix on your own?
- [ ] Yes I am willing to submit a pull request on my own!
Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Comment From: zrlw
Investigation Examples refer to https://github.com/apache/dubbo-samples/tree/master/2-advanced/dubbo-samples-triple-rest/dubbo-samples-triple-rest-basic 1. adjust testing server
@DubboService
public class DemoServiceImpl implements DemoService {
private static final StringBuffer finalString = new StringBuffer();
static {
IntStream.range(0, 20000).forEach(i -> finalString.append(i).append("Hello"));
}
@Override
public String hello(String name) {
return finalString + "Hello " + name;
}
@Override
public String hello(User user, int count) {
return finalString + "Hello " + user.getTitle() + ". " + user.getName() + ", " + count;
}
@Override
public String helloUser(User user) {
return finalString + "Hello " + user.getTitle() + ". " + user.getName();
}
}
- adjust test cases
@EnableDubbo
@RunWith(SpringRunner.class)
public class ConsumerIT {
private static final String HOST = System.getProperty("dubbo.address", "localhost");
private static final StringBuffer finalString = new StringBuffer();
static {
IntStream.range(0, 20000).forEach(i -> finalString.append(i).append("Hello"));
}
private final RestClient restClient = RestClient.create();
@DubboReference(url = "tri://${dubbo.address:localhost}:50052")
private DemoService demoService;
private static String toUri(String path) {
return "http://" + HOST + ":50052/org.apache.dubbo.rest.demo.DemoService" + path;
}
@Test
public void helloWithRpc() {
String result = demoService.hello("world");
Assert.assertEquals(finalString + "Hello world", result);
}
@Test
public void helloWithRest() {
String result = restClient.get().uri(toUri("/hello?name=world")).retrieve().body(String.class);
Assert.assertEquals("\"" + finalString + "Hello world\"", result);
}
@Test
public void helloWithRestAdvance() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("name", "Yang");
String result = restClient.post()
.uri(toUri("/hi.txt?title=Mr"))
.body(data)
.header("c", "3")
.retrieve()
.body(String.class);
Assert.assertEquals(finalString + "Hello Mr. Yang, 3", result);
}
@Test
public void helloWithBody() {
User user = new User();
user.setTitle("Mr");
user.setName("Yang");
String result = restClient.post()
.uri(toUri("/helloUser"))
.contentType(MediaType.APPLICATION_JSON)
.body(user)
.retrieve()
.body(String.class);
Assert.assertEquals("\"" + finalString + "Hello Mr. Yang\"", result);
}
}
- start BasicRestApplication
- debug each case of ConsumerIT one by one, using DEBUG mode is easy to find troubles if the test client runs slowly.