Skip to content

Commit 2bed30f

Browse files
jfallowsclaude
andauthored
fix(binding-http): reset streaming response on remote client disconnect (#1850)
* fix(binding-http): reset streaming response on remote client disconnect For an HTTP/1.1 server exchange whose request is already complete but whose response is still streaming (e.g. Server-Sent Events), a graceful client disconnect arrives as a network END. HttpExchange.onNetworkEnd only aborted the request and left the open response untouched, so the application's response stream was never reset and was only reaped by inactivity. This is inconsistent with HTTP/2, which resets the reply on END via cleanup. When the response is OPEN on network END, reset it and abort the network reply, mirroring the existing onDecodeBodyInvalid path. The responseState == OPEN guard preserves the valid HTTP half-close case (client closes its request side then reads the full response), which completes via the existing replyCloseOnFlush path. Add server IT client.close.during.response: a streaming response is reset (write aborted) when the client closes the connection mid-response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(binding-http): cover HTTP/2 reset of streaming response on client close Mirror the rfc7230 client.close.during.response scenario for HTTP/2: a server exchange whose request is complete but whose response is still streaming is reset (write aborted) when the client closes the connection mid-response. HTTP/2 already handles this via Http2Server cleanup on network END; this locks in parity with the HTTP/1.1 fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5408f41 commit 2bed30f

7 files changed

Lines changed: 215 additions & 2 deletions

File tree

  • runtime/binding-http/src
  • specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams
    • application
      • rfc7230/connection.management/client.close.during.response
      • rfc7540/connection.management/client.close.during.response
    • network
      • rfc7230/connection.management/client.close.during.response
      • rfc7540/connection.management/client.close.during.response

runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/stream/HttpServerFactory.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,7 +1900,7 @@ private void onNetworkEnd(
19001900

19011901
if (exchange != null)
19021902
{
1903-
exchange.onNetworkEnd(traceId);
1903+
exchange.onNetworkEnd(traceId, authorization);
19041904
}
19051905
else
19061906
{
@@ -2982,12 +2982,19 @@ private void doRequestFlush(
29822982
}
29832983

29842984
private void onNetworkEnd(
2985-
long traceId)
2985+
long traceId,
2986+
long authorization)
29862987
{
29872988
if (requestState != HttpExchangeState.CLOSED)
29882989
{
29892990
doRequestAbort(traceId, EMPTY_OCTETS);
29902991
}
2992+
2993+
if (responseState == HttpExchangeState.OPEN)
2994+
{
2995+
doResponseReset(traceId);
2996+
doNetworkAbort(traceId, authorization);
2997+
}
29912998
}
29922999

29933000
private void onNetworkAbort(

runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7230/server/ConnectionManagementIT.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ public void shouldRejectRequest() throws Exception
5959
k3po.finish();
6060
}
6161

62+
@Test
63+
@Configuration("server.yaml")
64+
@Specification({
65+
"${net}/client.close.during.response/client",
66+
"${app}/client.close.during.response/server" })
67+
public void shouldResetResponseWhenClientClosesDuringResponse() throws Exception
68+
{
69+
k3po.finish();
70+
}
71+
6272
@Test
6373
@Configuration("server.override.yaml")
6474
@Specification({

runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7540/server/ConnectionManagementIT.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,17 @@ public void clientSentWriteClose() throws Exception
266266
k3po.finish();
267267
}
268268

269+
@Test
270+
@Configuration("server.yaml")
271+
@Specification({
272+
"${net}/client.close.during.response/client",
273+
"${app}/client.close.during.response/server"
274+
})
275+
public void shouldResetResponseWhenClientClosesDuringResponse() throws Exception
276+
{
277+
k3po.finish();
278+
}
279+
269280
@Test
270281
@Configuration("server.yaml")
271282
@Specification({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Copyright 2021-2024 Aklivity Inc.
3+
#
4+
# Aklivity licenses this file to you under the Apache License,
5+
# version 2.0 (the "License"); you may not use this file except in compliance
6+
# with the License. You may obtain a copy of the License at:
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
17+
accept "zilla://streams/app0"
18+
option zilla:window 8192
19+
option zilla:transmission "duplex"
20+
accepted
21+
22+
read zilla:begin.ext ${http:beginEx()
23+
.typeId(zilla:id("http"))
24+
.header(":scheme", "http")
25+
.header(":method", "GET")
26+
.header(":path", "/")
27+
.header(":authority", "localhost:8080")
28+
.build()}
29+
30+
connected
31+
32+
read closed
33+
34+
write zilla:begin.ext ${http:beginEx()
35+
.typeId(zilla:id("http"))
36+
.header(":status", "200")
37+
.header("content-length", "13")
38+
.build()}
39+
write flush
40+
41+
write notify RESPONSE_STARTED
42+
43+
write aborted
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Copyright 2021-2024 Aklivity Inc.
3+
#
4+
# Aklivity licenses this file to you under the Apache License,
5+
# version 2.0 (the "License"); you may not use this file except in compliance
6+
# with the License. You may obtain a copy of the License at:
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
17+
accept "zilla://streams/app0"
18+
option zilla:window 8192
19+
option zilla:transmission "duplex"
20+
accepted
21+
22+
read zilla:begin.ext ${http:beginEx()
23+
.typeId(zilla:id("http"))
24+
.header(":method", "GET")
25+
.header(":scheme", "http")
26+
.header(":path", "/")
27+
.header(":authority", "localhost:8080")
28+
.build()}
29+
30+
connected
31+
32+
read closed
33+
34+
write zilla:begin.ext ${http:beginEx()
35+
.typeId(zilla:id("http"))
36+
.header(":status", "200")
37+
.header("content-length", "13")
38+
.build()}
39+
write flush
40+
41+
write notify RESPONSE_STARTED
42+
43+
write aborted
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# Copyright 2021-2024 Aklivity Inc.
3+
#
4+
# Aklivity licenses this file to you under the Apache License,
5+
# version 2.0 (the "License"); you may not use this file except in compliance
6+
# with the License. You may obtain a copy of the License at:
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
17+
connect "zilla://streams/net0"
18+
option zilla:window 8192
19+
option zilla:transmission "duplex"
20+
connected
21+
22+
write "GET / HTTP/1.1" "\r\n"
23+
write "Host: localhost:8080" "\r\n"
24+
write "\r\n"
25+
26+
read "HTTP/1.1 200 OK\r\n"
27+
28+
read await RESPONSE_STARTED
29+
30+
write close
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#
2+
# Copyright 2021-2024 Aklivity Inc.
3+
#
4+
# Aklivity licenses this file to you under the Apache License,
5+
# version 2.0 (the "License"); you may not use this file except in compliance
6+
# with the License. You may obtain a copy of the License at:
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
17+
connect "zilla://streams/net0"
18+
option zilla:window 8192
19+
option zilla:transmission "duplex"
20+
connected
21+
22+
# client connection preface
23+
write "PRI * HTTP/2.0\r\n"
24+
"\r\n"
25+
"SM\r\n"
26+
"\r\n"
27+
write flush
28+
29+
# server connection preface - SETTINGS frame
30+
read [0x00 0x00 0x12] # length = 18
31+
[0x04] # HTTP2 SETTINGS frame
32+
[0x00] # flags = 0x00
33+
[0x00 0x00 0x00 0x00] # stream_id = 0
34+
[0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
35+
[0x00 0x04 0x00 0x00 0x00 0x00] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 0
36+
[0x00 0x06 0x00 0x00 0x20 0x00] # SETTINGS_MAX_HEADER_LIST_SIZE(0x06) = 8192
37+
38+
write [0x00 0x00 0x0c] # length = 12
39+
[0x04] # HTTP2 SETTINGS frame
40+
[0x00] # flags = 0x00
41+
[0x00 0x00 0x00 0x00] # stream_id = 0
42+
[0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
43+
[0x00 0x04 0x00 0x00 0xff 0xff] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 65535
44+
write flush
45+
46+
read [0x00 0x00 0x00] # length = 0
47+
[0x04] # HTTP2 SETTINGS frame
48+
[0x01] # ACK
49+
[0x00 0x00 0x00 0x00] # stream_id = 0
50+
51+
write [0x00 0x00 0x13] # length = 19
52+
[0x01] # HEADERS frame
53+
[0x05] # END_STREAM | END_HEADERS
54+
[0x00 0x00 0x00 0x01] # stream_id = 1
55+
[0x82] # :method: GET
56+
[0x86] # :scheme: http
57+
[0x84] # :path: /
58+
[0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
59+
write flush
60+
61+
write [0x00 0x00 0x00] # length = 0
62+
[0x04] # HTTP2 SETTINGS frame
63+
[0x01] # ACK
64+
[0x00 0x00 0x00 0x00] # stream_id = 0
65+
write flush
66+
67+
read await RESPONSE_STARTED
68+
69+
write close

0 commit comments

Comments
 (0)