Skip to content

Fix leaked anyio streams in streamable_http#1991

Open
aabmass wants to merge 5 commits intomodelcontextprotocol:mainfrom
aabmass:fix-leaked-stream
Open

Fix leaked anyio streams in streamable_http#1991
aabmass wants to merge 5 commits intomodelcontextprotocol:mainfrom
aabmass:fix-leaked-stream

Conversation

@aabmass
Copy link

@aabmass aabmass commented Feb 4, 2026

Motivation and Context

Fixes a resource leak. I was seeing this in tests I am writing for a different issue (#421 (comment)). Before:

    | Traceback (most recent call last):
    |   File ".venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 183, in __del__
    |     warnings.warn(
    |     ~~~~~~~~~~~~~^
    |         f"Unclosed <{self.__class__.__name__} at {id(self):x}>",
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |     ...<2 lines>...
    |         source=self,
    |         ^^^^^^^^^^^^
    |     )
    |     ^
    | ResourceWarning: Unclosed <MemoryObjectReceiveStream at 7fbd5bf10a50>

How Has This Been Tested?

Added a new test as first commit in this PR which fails. It passes with the fix, and I was able to remove a bunch of pragma: no cover statements.

Breaking Changes

No

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@aabmass aabmass force-pushed the fix-leaked-stream branch 3 times, most recently from aad9e69 to aff1b13 Compare February 4, 2026 17:20
@aabmass aabmass marked this pull request as ready for review February 4, 2026 17:23
Comment on lines +624 to +625
finally:
await sse_stream_reader.aclose()
Copy link
Member

Choose a reason for hiding this comment

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

Why does the reader moves to the finally but not the writer?

Copy link
Author

Choose a reason for hiding this comment

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

On L569 it gets stored

                # Store writer reference so close_sse_stream() can close it
                self._sse_stream_writers[request_id] = sse_stream_writer

Should I be closing it here too, or does it need to outlive this function?

except Exception:
except Exception: # pragma: lax no cover
logger.exception("Error in standalone SSE response")
await self._clean_up_memory_streams(GET_STREAM_KEY)
Copy link
Member

Choose a reason for hiding this comment

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

Why this moves up?

Copy link
Author

Choose a reason for hiding this comment

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

This stays in the except block same as before. Should this happen in finally regardless of exception?

@aabmass aabmass requested a review from Kludex February 5, 2026 22:17
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