|
15 | 15 |
|
16 | 16 | # Define handler functions |
17 | 17 | async def handle_sse(request): |
18 | | - # Starlette also routes HEAD here: answer with GET's headers, not its never-ending stream |
| 18 | + # Starlette also routes HEAD here, and the stream would never end |
19 | 19 | if request.method == "HEAD": |
20 | | - response = Response(media_type="text/event-stream", headers={"Cache-Control": "no-store"}) |
21 | | - del response.headers["content-length"] |
22 | | - return response |
| 20 | + return await sse.head_response(request) |
23 | 21 | async with sse.connect_sse( |
24 | 22 | request.scope, request.receive, request._send |
25 | 23 | ) as streams: |
@@ -77,6 +75,9 @@ class SseServerTransport: |
77 | 75 | 2. handle_post_message() is an ASGI application which receives incoming POST |
78 | 76 | requests, which should contain client messages that link to a |
79 | 77 | previously-established SSE session. |
| 78 | +
|
| 79 | + head_response() answers a HEAD request to the SSE endpoint, which Starlette |
| 80 | + routes to the same endpoint as GET. |
80 | 81 | """ |
81 | 82 |
|
82 | 83 | _endpoint: str |
@@ -139,6 +140,27 @@ def __init__( |
139 | 140 | self._post_message_app = RequestBodyLimitMiddleware(self._handle_post_message, max_request_body_size) |
140 | 141 | logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") |
141 | 142 |
|
| 143 | + async def head_response(self, request: Request) -> Response: |
| 144 | + """Build the response to a HEAD request on the SSE endpoint. |
| 145 | +
|
| 146 | + An SSE stream never ends, so a HEAD served through `connect_sse()` would never complete. |
| 147 | + This runs the same Host/Origin validation and returns the headers a GET gets, with no body. |
| 148 | +
|
| 149 | + Args: |
| 150 | + request: The HEAD request. |
| 151 | +
|
| 152 | + Returns: |
| 153 | + The validation error response, or the SSE headers with no body. |
| 154 | + """ |
| 155 | + if error_response := await self._security.validate_request(request, is_post=False): |
| 156 | + return error_response |
| 157 | + response = Response( |
| 158 | + media_type="text/event-stream", headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"} |
| 159 | + ) |
| 160 | + # A Content-Length would claim an empty stream. |
| 161 | + del response.headers["content-length"] |
| 162 | + return response |
| 163 | + |
142 | 164 | @asynccontextmanager |
143 | 165 | async def connect_sse(self, scope: Scope, receive: Receive, send: Send): |
144 | 166 | if scope["type"] != "http": |
|
0 commit comments