stacklok / stacklok/toolhive

httpsse POST session-not-found 404 does not echo the request JSON-RPC id

Open
#6,042 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Follow-up to #5945 / #6031. Same asymmetry, different transport — and it was missed by that issue's audit, mine included.

Summary

pkg/transport/proxy/httpsse/http_proxy.go:555 returns a -32001 session-not-found 404 with a hardcoded null JSON-RPC id, even though the request is a POST that carries one:

func (p *HTTPSSEProxy) handlePostRequest(w http.ResponseWriter, r *http.Request) {   // :537
    if r.Method != http.MethodPost { ... }                                          // :539

    sessionID := query.Get("session_id")
    ...
    _, exists := p.sessionManager.Get(sessionID)
    if !exists {
        session.WriteNotFound(w, nil)      // <-- :555, id hardcoded nil
        return
    }
    ...
    body, err := io.ReadAll(r.Body)        // <-- ~:569, AFTER the lookup

So a client POSTing {"jsonrpc":"2.0","id":7,"method":"tools/list"} to /messages?session_id=<unknown> receives "id":null and cannot correlate the error with its request — exactly the complaint #5945 raised against the transparent proxy.

Why it was missed

#6031's audit table classified this site as a bodiless GET and concluded the transparent proxy was the only site discarding an available id. That was wrong on both counts: line 555 is inside handlePostRequest, and the id is present in a body that simply has not been read yet. The table is now corrected on that PR. Recording the error here too, because "we already audited this" is exactly the kind of claim that stops the next person looking.

The other two nil sites in the streamable proxy are as originally described and remain correct: streamable_proxy.go:433 (standalone-SSE GET) and :484 (DELETE) are genuinely bodiless. :1096 already echoes req.ID.Raw().

Why this is not a trivial repeat of #5945

In the transparent proxy the id was already parsed a few lines above the guard, so the fix was pure scoping. Here it is not: the session lookup deliberately runs before the body is read, so fixing it requires reading and parsing the body first — which changes error precedence.

Today, a request that is both malformed and carries an unknown session gets 404 (session checked first). Read the body first and it becomes 400 (parse error wins). That is a real behavioural decision, not an implementation detail:

  • 404-first (today) is arguably the better fail-safe: it tells the client to re-establish a session without the server having parsed attacker-controlled input under an unauthenticated session id.
  • 400-first would be more conventional JSON-RPC, and is what the transparent proxy now effectively does (it parses before the guard).

A third option avoids the choice entirely: parse the body only to extract the id, on a best-effort basis, and keep the 404 — falling back to a null id if the body is unparsable. That preserves precedence and still fixes correlation for the common case.

Also worth knowing

  • The existing unit test (http_proxy_test.go:524) asserts only the error code, not the id, so nothing currently pins either behaviour. Whichever way this goes, the test should assert the id so the decision is recorded.
  • Impact is limited by transport age: HTTP+SSE is deprecated in the MCP spec (2024-11-05 two-endpoint transport). That is a reason to keep the fix cheap, not a reason to leave a client unable to correlate errors.

Suggested scope

Small, but it needs the precedence decision made deliberately rather than as a side effect. Best-effort id extraction preserving the 404 is my guess at the right answer, but that is a call for whoever owns this transport.

Generated with Claude Code

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in pkg/transport/proxy/httpsse/http_proxy.go at handlePostRequest around lines 537-569, then inspect the existing session-not-found response and request parsing. Run the test at http_proxy_test.go:524 and extend it to cover the JSON-RPC id while preserving the deliberately chosen error precedence.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.