diff --git a/changelog.d/19530.misc b/changelog.d/19530.misc new file mode 100644 index 0000000000..9e5bc0fe04 --- /dev/null +++ b/changelog.d/19530.misc @@ -0,0 +1 @@ +Allow caching of the `/versions` and `/auth_metadata` public endpoints. diff --git a/synapse/http/server.py b/synapse/http/server.py index 226cb00831..2c235e04f4 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -861,7 +861,18 @@ def respond_with_json( encoder = _encode_json_bytes request.setHeader(b"Content-Type", b"application/json") - request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate") + # Insert a default Cache-Control header if the servlet hasn't already set one. The + # default directive tells both the client and any intermediary cache to not cache + # the response, which is a sensible default to have on most API endpoints. + # The absence `Cache-Control` header would mean that it's up to the clients and + # caching proxies mood to cache things if they want. This can be dangerous, which is + # why we explicitly set a "don't cache by default" policy. + # In practice, `no-store` should be enough, but having all three directives is more + # conservative in case we encounter weird, non-spec compliant caches. + # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives + # for more details. + if not request.responseHeaders.hasHeader(b"Cache-Control"): + request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate") if send_cors: set_cors_headers(request) @@ -901,7 +912,18 @@ def respond_with_json_bytes( request.setHeader(b"Content-Type", b"application/json") request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),)) - request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate") + # Insert a default Cache-Control header if the servlet hasn't already set one. The + # default directive tells both the client and any intermediary cache to not cache + # the response, which is a sensible default to have on most API endpoints. + # The absence `Cache-Control` header would mean that it's up to the clients and + # caching proxies mood to cache things if they want. This can be dangerous, which is + # why we explicitly set a "don't cache by default" policy. + # In practice, `no-store` should be enough, but having all three directives is more + # conservative in case we encounter weird, non-spec compliant caches. + # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives + # for more details. + if not request.responseHeaders.hasHeader(b"Cache-Control"): + request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate") if send_cors: set_cors_headers(request) diff --git a/synapse/rest/client/auth_metadata.py b/synapse/rest/client/auth_metadata.py index 702f550906..062b8ed13e 100644 --- a/synapse/rest/client/auth_metadata.py +++ b/synapse/rest/client/auth_metadata.py @@ -49,6 +49,25 @@ class AuthIssuerServlet(RestServlet): self._auth = hs.get_auth() async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: + # This endpoint is unauthenticated and the response only depends on + # the metadata we get from Matrix Authentication Service. Internally, + # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the + # response in memory anyway. Ideally we would follow any Cache-Control directive + # given by MAS, but this is fine for now. + # + # - `public` means it can be cached both in the browser and in caching proxies + # - `max-age` controls how long we cache on the browser side. 10m is sane enough + # - `s-maxage` controls how long we cache on the proxy side. Since caching + # proxies usually have a way to purge caches, it is fine to cache there for + # longer (1h), and issue cache invalidations in case we need it + # - `stale-while-revalidate` allows caching proxies to serve stale content while + # revalidating in the background. This is useful for making this request always + # 'snappy' to end users whilst still keeping it fresh + request.setHeader( + b"Cache-Control", + b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600", + ) + if self._config.mas.enabled: assert isinstance(self._auth, MasDelegatedAuth) return 200, {"issuer": await self._auth.issuer()} @@ -94,6 +113,25 @@ class AuthMetadataServlet(RestServlet): self._auth = hs.get_auth() async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: + # This endpoint is unauthenticated and the response only depends on + # the metadata we get from Matrix Authentication Service. Internally, + # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the + # response in memory anyway. Ideally we would follow any Cache-Control directive + # given by MAS, but this is fine for now. + # + # - `public` means it can be cached both in the browser and in caching proxies + # - `max-age` controls how long we cache on the browser side. 10m is sane enough + # - `s-maxage` controls how long we cache on the proxy side. Since caching + # proxies usually have a way to purge caches, it is fine to cache there for + # longer (1h), and issue cache invalidations in case we need it + # - `stale-while-revalidate` allows caching proxies to serve stale content while + # revalidating in the background. This is useful for making this request always + # 'snappy' to end users whilst still keeping it fresh + request.setHeader( + b"Cache-Control", + b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600", + ) + if self._config.mas.enabled: assert isinstance(self._auth, MasDelegatedAuth) return 200, await self._auth.auth_metadata() diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 23f5ffeedb..f8d7a1a4d9 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -81,6 +81,26 @@ class VersionsRestServlet(RestServlet): msc3575_enabled = await self.store.is_feature_enabled( user_id, ExperimentalFeature.MSC3575 ) + else: + # Allow caching of unauthenticated responses, as they only depend + # on server configuration which rarely changes. + # + # - `public` means it can be cached both in the browser and in caching proxies + # - `max-age` controls how long we cache on the browser side. 10m is sane enough + # - `s-maxage` controls how long we cache on the proxy side. Since caching + # proxies usually have a way to purge caches, it is fine to cache there for + # longer (1h), and issue cache invalidations in case we need it + # - `stale-while-revalidate` allows caching proxies to serve stale content while + # revalidating in the background. This is useful for making this request always + # 'snappy' to end users whilst still keeping it fresh + request.setHeader( + b"Cache-Control", + b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600", + ) + + # Tell caches to vary on the Authorization header, so that + # authenticated responses are not served from cache. + request.setHeader(b"Vary", b"Authorization") return ( 200,