Skip to content

Commit f4d86fe

Browse files
authored
Merge pull request #20 from efleming/fix/sse-already-sent-on-duplicate-get
2 parents c8deaa1 + ee653fa commit f4d86fe

3 files changed

Lines changed: 43 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
- Fix `Plug.Conn.AlreadySentError` when a second SSE GET arrives for an
4+
existing session. The conflict response (`409 -32000`) is now returned
5+
cleanly without attempting to write streaming headers on the sent conn.
6+
17
## 0.4.3 (2026-04-03)
28

39
- Fix invalid response when client request an invalid resource_uri

lib/phantom/plug.ex

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,22 @@ defmodule Phantom.Plug do
278278

279279
defp dispatch(%Plug.Conn{method: "GET"} = conn, opts) do
280280
if opts.pubsub do
281-
conn = maybe_track_session_stream(conn)
282-
session = conn.private.phantom.session
281+
case maybe_track_session_stream(conn) do
282+
%Plug.Conn{halted: true} = conn ->
283+
conn
283284

284-
conn
285-
|> put_resp_header("mcp-session-id", session.id)
286-
|> put_resp_header("cache-control", "no-cache, no-transform")
287-
|> put_resp_content_type("text/event-stream")
288-
|> put_resp_header("connection", "keep-alive")
289-
|> put_resp_header("x-accel-buffering", "no")
290-
|> send_chunked(202)
291-
|> stream_loop(opts)
285+
conn ->
286+
session = conn.private.phantom.session
287+
288+
conn
289+
|> put_resp_header("mcp-session-id", session.id)
290+
|> put_resp_header("cache-control", "no-cache, no-transform")
291+
|> put_resp_content_type("text/event-stream")
292+
|> put_resp_header("connection", "keep-alive")
293+
|> put_resp_header("x-accel-buffering", "no")
294+
|> send_chunked(202)
295+
|> stream_loop(opts)
296+
end
292297
else
293298
conn
294299
|> put_status(405)

test/phantom/plug_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,28 @@ defmodule Phantom.PlugTest do
299299
assert_sse_connected()
300300
assert Phantom.Tracker.list_sessions() != []
301301
end
302+
303+
test "second GET on same session returns 409 without raising AlreadySentError" do
304+
session_id = "019dd3d8-0000-0000-0000-000000000001"
305+
306+
:get
307+
|> conn("/mcp")
308+
|> put_req_header("accept", "text/event-stream")
309+
|> call(session_id: session_id)
310+
311+
assert_sse_connected()
312+
313+
:get
314+
|> conn("/mcp")
315+
|> put_req_header("accept", "text/event-stream")
316+
|> call(session_id: session_id)
317+
318+
assert_receive {:conn, conn}
319+
assert conn.status == 409
320+
error = JSON.decode!(conn.resp_body)
321+
assert error["error"]["code"] == -32000
322+
assert error["error"]["message"] == "Only one SSE stream is allowed per session"
323+
end
302324
end
303325

304326
test "handles prompt responses" do

0 commit comments

Comments
 (0)