diff --git a/Makefile b/Makefile index 0c71f89..c56fa9e 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ style: sort-imports format ## perform code style format (black, isort) .PHONY: lint lint: ## check style with ruff - ruff $(pkg_src) $(tests_src) + ruff check $(pkg_src) $(tests_src) .PHONY: mypy mypy: ## check type hint annotations diff --git a/pyproject.toml b/pyproject.toml index fb03761..af31a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ dev = [ "asgi-lifespan", "psutil", "black>=23.11.0", + "ruff", ] [tool.isort] diff --git a/sse_starlette/sse.py b/sse_starlette/sse.py index 2104cb7..6863aad 100644 --- a/sse_starlette/sse.py +++ b/sse_starlette/sse.py @@ -20,6 +20,7 @@ from starlette.concurrency import iterate_in_threadpool from starlette.responses import Response from starlette.types import Receive, Scope, Send +from starlette.datastructures import MutableHeaders _log = logging.getLogger(__name__) @@ -196,14 +197,18 @@ def __init__( self.data_sender_callable = data_sender_callable self.send_timeout = send_timeout - _headers: dict[str, str] = {} + _headers = MutableHeaders() if headers is not None: # pragma: no cover _headers.update(headers) - # mandatory for servers-sent events headers + # "The no-store response directive indicates that any caches of any kind (private or shared) + # should not store this response." + # -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control # allow cache control header to be set by user to support fan out proxies # https://www.fastly.com/blog/server-sent-events-fastly - _headers.setdefault("Cache-Control", "no-cache") + + _headers.setdefault("Cache-Control", "no-store") + # mandatory for servers-sent events headers _headers["Connection"] = "keep-alive" _headers["X-Accel-Buffering"] = "no" diff --git a/tests/test_event_source_response.py b/tests/test_event_source_response.py index fa69e3f..c67fc1c 100644 --- a/tests/test_event_source_response.py +++ b/tests/test_event_source_response.py @@ -171,3 +171,36 @@ async def receive(): await response({}, receive, send) assert cleanup + + +def test_headers_with_override(): + generator = range(1, 10) + response = EventSourceResponse(generator, ping=0.2) # type: ignore + default_headers = dict((h.decode(), v.decode()) for h, v in response.raw_headers) + assert default_headers == { + "cache-control": "no-store", + "x-accel-buffering": "no", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + } + + custom_headers = { + # cache-control can be overridden + "cache-control": "no-cache", + # x-accel-buffering and connection cannot + "x-accel-buffering": "yes", + "connection": "close", + # other headers are allowed + "x-my-special-header": "37", + } + generator = range(1, 10) + response = EventSourceResponse(generator, headers=custom_headers, ping=0.2) # type: ignore + override_headers = dict((h.decode(), v.decode()) for h, v in response.raw_headers) + + assert override_headers == { + "cache-control": "no-cache", + "x-accel-buffering": "no", + "connection": "keep-alive", + "x-my-special-header": "37", + "content-type": "text/event-stream; charset=utf-8", + }