Skip to content

fix(api): chat/sessions returns HTTP 200 with an SSE error event when session creation is refused before streaming #652

Description

@ericfitz

Summary

POST /threat_models/{threat_model_id}/chat/sessions reports a synchronous, pre-stream failure as
HTTP 200 with the error delivered inside the SSE body:

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: error
data: {"code":"session_creation_failed","message":"threat model has reached the maximum of 50 active sessions"}

Nothing was streamed before the error — the request was rejected outright. A caller checking the
status code sees success.

Observed in CATS run 20260730T220551Z (tests 55707, 55710, 55711, 55717, 55745, 55746, 55791,
55792, 55839, 55983, 55984 and others).

Why it matters

  • A client cannot distinguish "session created, stream follows" from "request refused" without
    parsing the stream, which defeats ordinary HTTP error handling, retry policy and middleware.
  • The session cap is a resource-exhaustion condition with an established status code (429, or 409
    if treated as a conflict). Both are already documented for this operation.
  • SSE's usual justification does not apply here: headers only have to be committed before the first
    event is written. This failure is known before any event, so the status can still be set.

Suggested fix

Perform session-creation validation (cap check, authorization, diagram existence) before
writing SSE headers, and return a normal JSON error response with 429/409/400 as appropriate. Only
switch to text/event-stream once creation has succeeded and the stream is genuinely starting.
Errors that arise mid-stream should keep using event: error, which is correct once headers are
committed.

Secondary: the cap is reached by the campaign itself

Sessions accumulate across a campaign until the 50-session ceiling is hit, after which every
remaining chat/sessions test exercises only the rejection path. That is a coverage loss of the
same family as #651 and #633. Either the sessions need a shorter inactivity timeout for fuzzing, or
the seed/teardown hook should reap them, or the path should be fuzzed scoped.

Related: #651 (anchor decoy coverage loss), #633 (DELETE /me coverage loss).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions