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

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();
    }
}
  1. 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);
    }
}
  1. start BasicRestApplication
  2. debug each case of ConsumerIT one by one, using DEBUG mode is easy to find troubles if the test client runs slowly.