Add chat-ui-htmx module: streaming chat page on Spring AI 2.0.2 with htmx and its SSE extension (vendored), MessageChatMemoryAdvisor keyed by the HTTP session, a Stop button, and the traps measured: EventSource strips one leading space per data line, model text must be HTML-escaped, a cancelled stream left the upstream connection open in 7 of 8 runs until an OkHttp interceptor calls Call.cancel (8 of 8 closed after), and the SDK push source ignores back-pressure (limitRate changes only the demand; a bounded buffer works). The model is a stub OpenAI-compatible SSE server written for the module; no real provider was run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Kg9RWSTJzyz6KP9X
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
$ GET /chat/stream?message=hello world (raw lines, first 14 and last 5)
|
||||
event:token
|
||||
data: [stub]
|
||||
|
||||
event:token
|
||||
data: saw
|
||||
|
||||
event:token
|
||||
data: 2
|
||||
|
||||
event:token
|
||||
data: messages.
|
||||
|
||||
event:token
|
||||
data: Earlier
|
||||
...
|
||||
data: world
|
||||
<blank>
|
||||
event:done
|
||||
data:
|
||||
<blank>
|
||||
event names seen: [token, done]
|
||||
joined token text as EventSource delivers it: [stub] saw 2 messages. Earlier user turns: none. You said: hello world
|
||||
same text if the server did not pad: [stub]saw2messages.Earlieruserturns:none.Yousaid:helloworld
|
||||
@@ -0,0 +1,4 @@
|
||||
session A, turn 1: [stub] saw 2 messages. Earlier user turns: none. You said: My name is Asha and I like tea
|
||||
session A, turn 2: [stub] saw 4 messages. Earlier user turns: My name is Asha and I like tea. You said: What did I just tell you?
|
||||
session B, turn 1: [stub] saw 2 messages. Earlier user turns: none. You said: What did I just tell you?
|
||||
session A, turn 3: [stub] saw 6 messages. Earlier user turns: My name is Asha and I like tea | What did I just tell you?. You said: And now?
|
||||
@@ -0,0 +1 @@
|
||||
data sent to the browser: Here is <b>bold</b> and <img src=x onerror=alert(1)> & done
|
||||
@@ -0,0 +1,4 @@
|
||||
client read 40000 bytes then closed the socket
|
||||
Flux cancelled on the server: 1
|
||||
stub saw the upstream connection close: 1 (within 50 ms)
|
||||
stub chunks written for this request: 5 of 400 requested
|
||||
@@ -0,0 +1,4 @@
|
||||
plain OkHttp 4.12.0, 10 runs each, stub streaming 60 chunks at 20 ms, reader thread blocked in a read
|
||||
response.close() threw: java.lang.IllegalStateException: Unbalanced enter/exit
|
||||
response.close() from another thread: upstream connection closed in 0 of 10 runs
|
||||
call.cancel() from another thread: upstream connection closed in 10 of 10 runs
|
||||
@@ -0,0 +1,18 @@
|
||||
== turn 1 (session A)
|
||||
A1: '[stub] saw 2 messages. Earlier user turns: none. You said: My name is Asha and I like tea'
|
||||
== turn 2 (session A, same cookie)
|
||||
A2: '[stub] saw 4 messages. Earlier user turns: My name is Asha and I like tea. You said: What did I just tell you?'
|
||||
cookies: ['JSESSIONID']
|
||||
== turn 1 (session B, fresh browser context, no cookie)
|
||||
B1: '[stub] saw 2 messages. Earlier user turns: none. You said: What did I just tell you?'
|
||||
== New conversation button in session A, then ask again
|
||||
A3: '[stub] saw 2 messages. Earlier user turns: none. You said: Do you remember me?'
|
||||
== model output containing markup
|
||||
rendered text: 'Here is <b>bold</b> and <img src=x onerror=alert(1)> & done'
|
||||
<b> elements created inside the answer: 0
|
||||
<img> elements created inside the answer: 0
|
||||
alert dialogs fired: 0
|
||||
== stop button
|
||||
text at click : 'w1 w2 w3 w4 w5 w6 w7 w8 w9 w10 w11'
|
||||
text 1.5 s on : 'w1 w2 w3 w4 w5 w6 w7 w8 w9 w10 w11 w12'
|
||||
grew after stop: False
|
||||
@@ -0,0 +1,6 @@
|
||||
-- app log (the turn started by the Stop-button test)
|
||||
turn 6 start conversation=def8b65c-b58a-4677-b149-e71575426ebd message=count 200
|
||||
turn 6 CANCELLED by client after 14 tokens
|
||||
-- stub log
|
||||
stub: request #6 model=stub-gpt stream=true messages=6 roles=system,user,assistant,user,assistant,user latest="count 200"
|
||||
stub: stream #6 client closed the connection after 15 chunks
|
||||
@@ -0,0 +1,21 @@
|
||||
== chunk delay 20 ms, 8 runs, app args: --chat.cancel-fix=false
|
||||
run 1: upstream closed
|
||||
run 2: upstream LEFT OPEN
|
||||
run 3: upstream LEFT OPEN
|
||||
run 4: upstream LEFT OPEN
|
||||
run 5: upstream LEFT OPEN
|
||||
run 6: upstream LEFT OPEN
|
||||
run 7: upstream LEFT OPEN
|
||||
run 8: upstream LEFT OPEN
|
||||
upstream closed: 1 of 8, left open: 7 of 8
|
||||
|
||||
== chunk delay 20 ms, 8 runs, app args: (none)
|
||||
run 1: upstream closed
|
||||
run 2: upstream closed
|
||||
run 3: upstream closed
|
||||
run 4: upstream closed
|
||||
run 5: upstream closed
|
||||
run 6: upstream closed
|
||||
run 7: upstream closed
|
||||
run 8: upstream closed
|
||||
upstream closed: 8 of 8, left open: 0 of 8
|
||||
@@ -0,0 +1,45 @@
|
||||
== chat.backpressure=none (stub delay 0 ms, 600 chunks x 32 KiB, client reads 64 KiB every 0.2 s)
|
||||
t= 1.0s client got 0 KiB stub: chunksWritten=0 finished=0 abortedByClient=0 app heap after GC: used 34669K
|
||||
t= 2.8s client got 197 KiB stub: chunksWritten=452 finished=0 abortedByClient=0 app heap after GC: used 134096K
|
||||
t= 5.5s client got 896 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 86306K
|
||||
t= 8.9s client got 1832 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 109343K
|
||||
t=13.3s client got 2967 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 116565K
|
||||
-- app log (demand + outcome):
|
||||
213 web-side demand request(1)
|
||||
213 model-side demand request(1)
|
||||
1 CANCELLED by client after 213 tokens
|
||||
|
||||
== chat.backpressure=limitRate (stub delay 0 ms, 600 chunks x 32 KiB, client reads 64 KiB every 0.2 s)
|
||||
t= 1.0s client got 0 KiB stub: chunksWritten=0 finished=0 abortedByClient=0 app heap after GC: used 42630K
|
||||
t= 2.8s client got 242 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 86598K
|
||||
t= 5.2s client got 896 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 86745K
|
||||
t= 8.5s client got 1768 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 109571K
|
||||
t=12.9s client got 2967 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 116491K
|
||||
-- app log (demand + outcome):
|
||||
213 web-side demand request(1)
|
||||
35 model-side demand request(6)
|
||||
1 model-side demand request(8)
|
||||
1 CANCELLED by client after 213 tokens
|
||||
|
||||
== chat.backpressure=slow (stub delay 0 ms, 600 chunks x 32 KiB, client reads 64 KiB every 0.2 s)
|
||||
t= 1.0s client got 0 KiB stub: chunksWritten=0 finished=0 abortedByClient=0 app heap after GC: used 40942K
|
||||
t= 2.6s client got 72 KiB stub: chunksWritten=218 finished=0 abortedByClient=0 app heap after GC: used 146705K
|
||||
t= 5.3s client got 787 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 111009K
|
||||
t= 8.6s client got 1659 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 109188K
|
||||
t=13.0s client got 2858 KiB stub: chunksWritten=603 finished=1 abortedByClient=0 app heap after GC: used 119593K
|
||||
-- app log (demand + outcome):
|
||||
221 model-side demand request(1)
|
||||
220 web-side demand request(1)
|
||||
1 CANCELLED by client after 220 tokens
|
||||
|
||||
== chat.backpressure=bounded (stub delay 0 ms, 600 chunks x 32 KiB, client reads 64 KiB every 0.2 s)
|
||||
t= 1.0s client got 0 KiB stub: chunksWritten=0 finished=0 abortedByClient=0 app heap after GC: used 61061K
|
||||
t= 2.7s client got 133 KiB stub: chunksWritten=256 finished=0 abortedByClient=0 app heap after GC: used 58420K
|
||||
t= 5.1s client got 787 KiB stub: chunksWritten=256 finished=0 abortedByClient=1 app heap after GC: used 58420K
|
||||
t= 8.5s client got 1659 KiB stub: chunksWritten=256 finished=0 abortedByClient=1 app heap after GC: used 30251K
|
||||
t=12.8s client got 2858 KiB stub: chunksWritten=256 finished=0 abortedByClient=1 app heap after GC: used 30381K
|
||||
-- app log (demand + outcome):
|
||||
144 web-side demand request(1)
|
||||
1 model-side demand request(unbounded)
|
||||
1 error reactor.core.Exceptions$OverflowException: The receiver is overrun by more signals than expected (bounded queue...)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
$ curl -sN "localhost:38180/chat/stream?message=count 700" (700 tokens x 50 ms = 35 s)
|
||||
<blank>
|
||||
event:done
|
||||
data:
|
||||
<blank>
|
||||
elapsed: 38 s
|
||||
turn 1 complete, 700 tokens
|
||||
@@ -0,0 +1 @@
|
||||
stream failed: IllegalArgumentException: conversationId cannot be null
|
||||
@@ -0,0 +1,2 @@
|
||||
turn 1 'warm' completed, turn 2 'count 300' cancelled after a few tokens, then turn 3:
|
||||
[stub] saw 5 messages. Earlier user turns: warm | count 300. You said: What do you remember?
|
||||
Reference in New Issue
Block a user