Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

- Fix `Plug.Conn.AlreadySentError` when a second SSE GET arrives for an
existing session. The conflict response (`409 -32000`) is now returned
cleanly without attempting to write streaming headers on the sent conn.

## 0.4.3 (2026-04-03)

- Fix invalid response when client request an invalid resource_uri
Expand Down
25 changes: 15 additions & 10 deletions lib/phantom/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -278,17 +278,22 @@ defmodule Phantom.Plug do

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

conn
|> put_resp_header("mcp-session-id", session.id)
|> put_resp_header("cache-control", "no-cache, no-transform")
|> put_resp_content_type("text/event-stream")
|> put_resp_header("connection", "keep-alive")
|> put_resp_header("x-accel-buffering", "no")
|> send_chunked(202)
|> stream_loop(opts)
conn ->
session = conn.private.phantom.session

conn
|> put_resp_header("mcp-session-id", session.id)
|> put_resp_header("cache-control", "no-cache, no-transform")
|> put_resp_content_type("text/event-stream")
|> put_resp_header("connection", "keep-alive")
|> put_resp_header("x-accel-buffering", "no")
|> send_chunked(202)
|> stream_loop(opts)
end
else
conn
|> put_status(405)
Expand Down
22 changes: 22 additions & 0 deletions test/phantom/plug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@ defmodule Phantom.PlugTest do
assert_sse_connected()
assert Phantom.Tracker.list_sessions() != []
end

test "second GET on same session returns 409 without raising AlreadySentError" do
session_id = "019dd3d8-0000-0000-0000-000000000001"

:get
|> conn("/mcp")
|> put_req_header("accept", "text/event-stream")
|> call(session_id: session_id)

assert_sse_connected()

:get
|> conn("/mcp")
|> put_req_header("accept", "text/event-stream")
|> call(session_id: session_id)

assert_receive {:conn, conn}
assert conn.status == 409
error = JSON.decode!(conn.resp_body)
assert error["error"]["code"] == -32000
assert error["error"]["message"] == "Only one SSE stream is allowed per session"
end
end

test "handles prompt responses" do
Expand Down
Loading