Skip to content

Commit 4c475dc

Browse files
authored
Allow the caching of the /versions and /auth_metadata endpoints (element-hq#19530)
Can be reviewed commit by commit. This sets caching headers on the /versions and /auth_metadata endpoints to: - allow clients to cache the response for up to 10 minutes (`max-age=600`) - allow proxies to cache the response for up to an hour (`s-maxage=3600`) - make proxies serve stale response for up to an hour (`s-maxage=3600`) but make them refresh their response after 10 minutes (`stale-while-revalidate=600`) so that we always have a snappy response to client, but also have fresh responses most of the time - only cache the response for unauthenticated requests on /versions (`Vary: Authorization`) I'm not too worried about the 1h TTL on the proxy side, as with the `stale-while-revalidate` directive, one just needs to do two requests after 10 minutes to get a fresh response from the cache. The reason we want this, is that clients usually load this right away, leading to a lot of traffic from people just loading the Element Web login screen with the default config. This is currently routed to `client_readers` on matrix.org (and ESS) which can be overwhelmed for other reasons, leading to slow response times on those endpoints (3s+). Overwhelmed workers shouldn't prevent people from logging in, and shouldn't result in a long loading spinner in clients. This PR allows caching proxies (like Cloudflare) to publicly cache the unauthenticated response of those two endpoints and make it load quicker, reducing server load as well.
1 parent 3ce5508 commit 4c475dc

4 files changed

Lines changed: 83 additions & 2 deletions

File tree

changelog.d/19530.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow caching of the `/versions` and `/auth_metadata` public endpoints.

synapse/http/server.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,18 @@ def respond_with_json(
861861
encoder = _encode_json_bytes
862862

863863
request.setHeader(b"Content-Type", b"application/json")
864-
request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
864+
# Insert a default Cache-Control header if the servlet hasn't already set one. The
865+
# default directive tells both the client and any intermediary cache to not cache
866+
# the response, which is a sensible default to have on most API endpoints.
867+
# The absence `Cache-Control` header would mean that it's up to the clients and
868+
# caching proxies mood to cache things if they want. This can be dangerous, which is
869+
# why we explicitly set a "don't cache by default" policy.
870+
# In practice, `no-store` should be enough, but having all three directives is more
871+
# conservative in case we encounter weird, non-spec compliant caches.
872+
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
873+
# for more details.
874+
if not request.responseHeaders.hasHeader(b"Cache-Control"):
875+
request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
865876

866877
if send_cors:
867878
set_cors_headers(request)
@@ -901,7 +912,18 @@ def respond_with_json_bytes(
901912

902913
request.setHeader(b"Content-Type", b"application/json")
903914
request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
904-
request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
915+
# Insert a default Cache-Control header if the servlet hasn't already set one. The
916+
# default directive tells both the client and any intermediary cache to not cache
917+
# the response, which is a sensible default to have on most API endpoints.
918+
# The absence `Cache-Control` header would mean that it's up to the clients and
919+
# caching proxies mood to cache things if they want. This can be dangerous, which is
920+
# why we explicitly set a "don't cache by default" policy.
921+
# In practice, `no-store` should be enough, but having all three directives is more
922+
# conservative in case we encounter weird, non-spec compliant caches.
923+
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
924+
# for more details.
925+
if not request.responseHeaders.hasHeader(b"Cache-Control"):
926+
request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
905927

906928
if send_cors:
907929
set_cors_headers(request)

synapse/rest/client/auth_metadata.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ def __init__(self, hs: "HomeServer"):
4949
self._auth = hs.get_auth()
5050

5151
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
52+
# This endpoint is unauthenticated and the response only depends on
53+
# the metadata we get from Matrix Authentication Service. Internally,
54+
# MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
55+
# response in memory anyway. Ideally we would follow any Cache-Control directive
56+
# given by MAS, but this is fine for now.
57+
#
58+
# - `public` means it can be cached both in the browser and in caching proxies
59+
# - `max-age` controls how long we cache on the browser side. 10m is sane enough
60+
# - `s-maxage` controls how long we cache on the proxy side. Since caching
61+
# proxies usually have a way to purge caches, it is fine to cache there for
62+
# longer (1h), and issue cache invalidations in case we need it
63+
# - `stale-while-revalidate` allows caching proxies to serve stale content while
64+
# revalidating in the background. This is useful for making this request always
65+
# 'snappy' to end users whilst still keeping it fresh
66+
request.setHeader(
67+
b"Cache-Control",
68+
b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
69+
)
70+
5271
if self._config.mas.enabled:
5372
assert isinstance(self._auth, MasDelegatedAuth)
5473
return 200, {"issuer": await self._auth.issuer()}
@@ -94,6 +113,25 @@ def __init__(self, hs: "HomeServer"):
94113
self._auth = hs.get_auth()
95114

96115
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
116+
# This endpoint is unauthenticated and the response only depends on
117+
# the metadata we get from Matrix Authentication Service. Internally,
118+
# MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
119+
# response in memory anyway. Ideally we would follow any Cache-Control directive
120+
# given by MAS, but this is fine for now.
121+
#
122+
# - `public` means it can be cached both in the browser and in caching proxies
123+
# - `max-age` controls how long we cache on the browser side. 10m is sane enough
124+
# - `s-maxage` controls how long we cache on the proxy side. Since caching
125+
# proxies usually have a way to purge caches, it is fine to cache there for
126+
# longer (1h), and issue cache invalidations in case we need it
127+
# - `stale-while-revalidate` allows caching proxies to serve stale content while
128+
# revalidating in the background. This is useful for making this request always
129+
# 'snappy' to end users whilst still keeping it fresh
130+
request.setHeader(
131+
b"Cache-Control",
132+
b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
133+
)
134+
97135
if self._config.mas.enabled:
98136
assert isinstance(self._auth, MasDelegatedAuth)
99137
return 200, await self._auth.auth_metadata()

synapse/rest/client/versions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
8181
msc3575_enabled = await self.store.is_feature_enabled(
8282
user_id, ExperimentalFeature.MSC3575
8383
)
84+
else:
85+
# Allow caching of unauthenticated responses, as they only depend
86+
# on server configuration which rarely changes.
87+
#
88+
# - `public` means it can be cached both in the browser and in caching proxies
89+
# - `max-age` controls how long we cache on the browser side. 10m is sane enough
90+
# - `s-maxage` controls how long we cache on the proxy side. Since caching
91+
# proxies usually have a way to purge caches, it is fine to cache there for
92+
# longer (1h), and issue cache invalidations in case we need it
93+
# - `stale-while-revalidate` allows caching proxies to serve stale content while
94+
# revalidating in the background. This is useful for making this request always
95+
# 'snappy' to end users whilst still keeping it fresh
96+
request.setHeader(
97+
b"Cache-Control",
98+
b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
99+
)
100+
101+
# Tell caches to vary on the Authorization header, so that
102+
# authenticated responses are not served from cache.
103+
request.setHeader(b"Vary", b"Authorization")
84104

85105
return (
86106
200,

0 commit comments

Comments
 (0)