Skip to content

fix: prevent "Server not initialized" race on first request - #198

Merged
zoedsoupe merged 1 commit into
zoedsoupe:mainfrom
uhadmin:fix/init-race-streamable-http
Jul 15, 2026
Merged

zoedsoupe merged 1 commit into
zoedsoupe:mainfrom
uhadmin:fix/init-race-streamable-http

Conversation

@dbrown2642

Copy link
Copy Markdown
Contributor

Problem

On Streamable HTTP, the client sends notifications/initialized and its first request (e.g. tools/list) as two separate, near-simultaneous HTTP requests. The client sends them in order, but the transport doesn't guarantee order, so tools/list sometimes lands before the notification.

When it does, the session rejects it with "Server not initialized" (gate, error). A client that doesn't retry then stalls. In my case, the Claude Desktop client hangs ~30s, then sends notifications/cancelled.

The spec lets a client send requests as soon as the server responds to initialize, so this is stricter than the spec requires.

Solution

Mark the session initialized on the initialize response, not only in the notifications/initialized handler. Adds a test for a request arriving after initialize but before the notification.

Rationale

Matches the official SDKs, which mark the session ready on the initialize response:

The spec says SHOULD NOT, not MUST NOT, so the notification isn't a hard gate.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Marks Streamable HTTP sessions as initialized as soon as initialize completes, instead of waiting for notifications/initialized, so early client requests are accepted rather than rejected with “Server not initialized.” Adds a regression test covering a request that arrives after initialize but before the initialization notification.

Walkthrough

In the initialize request handler within lib/anubis/server/session.ex, the state update now includes initialized: true alongside client_capabilities. Previously, the session was only marked initialized after receiving a notifications/initialized notification. A new test in test/anubis/server/session_test.exs verifies that a tools/list request is processed successfully immediately after an initialize request, without waiting for the notification.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes the race fix on first request.
Description check ✅ Passed The description matches the template and includes Problem, Solution, and Rationale with concrete details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/anubis/server/session.ex (1)

669-675: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

P1: initialized: true now opens the gate before module.init/2 runs.

Lines 674-675 let the first post-initialize request/notification through, but the server setup callback still only runs in handle_notification("notifications/initialized") on Lines 891-915. That means the race fixed by this PR can now route work through handle_request/2 with a pre-init frame. The auto-init path already avoids this by calling maybe_call_init/3 before serving traffic, so the normal init path should do the same (or split “accept requests” from “server callback completed” into separate flags) to avoid this new boss fight.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3a2f2d3c-143a-4bdf-9f18-6cf950b951ea

📥 Commits

Reviewing files that changed from the base of the PR and between 2c641a0 and da0e33a.

📒 Files selected for processing (2)
  • lib/anubis/server/session.ex
  • test/anubis/server/session_test.exs

@zoedsoupe
zoedsoupe merged commit e84624c into zoedsoupe:main Jul 15, 2026
9 checks passed
@zoedsoupe zoedsoupe mentioned this pull request Jul 15, 2026
zoedsoupe added a commit that referenced this pull request Jul 16, 2026
🚀 Want to release this?
---


##
[1.7.0](v1.6.2...v1.7.0)
(2026-07-16)


### Features

* **streamable_http:** per-subscriber metadata and targeted sends
([#218](#218))
([c606658](c606658))


### Bug Fixes

* Forward configured :headers on the DELETE session-teardown request
(follow-up to
[#180](#180))
([#213](#213))
([b32134a](b32134a))
* prevent "Server not initialized" race on first request
([#198](#198))
([e84624c](e84624c))
* **server:** resolve session names via Registry to prevent
atom-exhaustion DoS
([#188](#188))
([17e4a6d](17e4a6d))
* **session:** trap_exit so terminate/2 runs on supervisor shutdown
([#209](#209))
([6335cf4](6335cf4))
* **streamable_http:** don't close superseded SSE handler to prevent
reconnect flap
([#215](#215))
([a1e0ce6](a1e0ce6))


### Continuous Integration

* add new elixir versions
([3b636a8](3b636a8))
* add pr-quality workflow
([c0ca08f](c0ca08f))
* fix zig correct version for burrito
([6e410bd](6e410bd))
* use mlugg/setup-zig 0.15.2 in release-please auto build job
([2ed6187](2ed6187))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
zoedsoupe pushed a commit that referenced this pull request Jul 16, 2026
## Problem

On Streamable HTTP, the client sends `notifications/initialized` and its
first request (e.g. `tools/list`) as two separate, near-simultaneous
HTTP requests. The client sends them in order, but the transport doesn't
guarantee order, so `tools/list` sometimes lands before the
notification.

When it does, the session rejects it with `"Server not initialized"`
([gate](https://github.com/zoedsoupe/anubis-mcp/blob/main/lib/anubis/server/session.ex#L603-L605),
[error](https://github.com/zoedsoupe/anubis-mcp/blob/main/lib/anubis/server/session.ex#L635-L645)).
A client that doesn't retry then stalls. In my case, the Claude Desktop
client hangs ~30s, then sends `notifications/cancelled`.

The spec lets a client send requests as soon as the server responds to
`initialize`, so this is stricter than the spec requires.

## Solution

Mark the session initialized on the `initialize` response, not only in
the `notifications/initialized` handler. Adds a test for a request
arriving after `initialize` but before the notification.

## Rationale

Matches the official SDKs, which mark the session ready on the
`initialize` response:

- TypeScript
([streamableHttp.ts#L501](https://github.com/modelcontextprotocol/typescript-sdk/blob/caa25503cdfc449d116c204e866bccc2617d7037/src/server/streamableHttp.ts#L501))
- Python
([#1478](modelcontextprotocol/python-sdk#1478))
- Rust
([#788](modelcontextprotocol/rust-sdk#788))

The spec says **SHOULD NOT**, not **MUST NOT**, so the notification
isn't a hard gate.
zoedsoupe added a commit that referenced this pull request Jul 16, 2026
🚀 Want to release this?
---


##
[1.7.0](v1.6.2...v1.7.0)
(2026-07-16)


### Features

* **streamable_http:** per-subscriber metadata and targeted sends
([#218](#218))
([d6cf7b1](d6cf7b1))


### Bug Fixes

* Forward configured :headers on the DELETE session-teardown request
(follow-up to
[#180](#180))
([#213](#213))
([4edd2c0](4edd2c0))
* prevent "Server not initialized" race on first request
([#198](#198))
([6bb60f9](6bb60f9))
* **server:** resolve session names via Registry to prevent
atom-exhaustion DoS
([#188](#188))
([fdbc238](fdbc238))
* **session:** trap_exit so terminate/2 runs on supervisor shutdown
([#209](#209))
([a224c00](a224c00))
* **streamable_http:** don't close superseded SSE handler to prevent
reconnect flap
([#215](#215))
([e1cc4a8](e1cc4a8))


### Continuous Integration

* add new elixir versions
([26dd267](26dd267))
* add pr-quality workflow
([d67eaa9](d67eaa9))
* fix zig correct version for burrito
([afec768](afec768))
* use mlugg/setup-zig 0.15.2 in release-please auto build job
([428cace](428cace))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants