118118# Maximum allowed timeout_ms for download and thumbnail requests
119119MAXIMUM_ALLOWED_MAX_TIMEOUT_MS = 60_000
120120
121+ # The ETag header value to use for immutable media. This can be anything.
122+ _IMMUTABLE_ETAG = "1"
123+
121124
122125def respond_404 (request : SynapseRequest ) -> None :
123126 assert request .path is not None
@@ -224,12 +227,7 @@ def _quote(x: str) -> str:
224227
225228 request .setHeader (b"Content-Disposition" , disposition .encode ("ascii" ))
226229
227- # cache for at least a day.
228- # XXX: we might want to turn this off for data we don't want to
229- # recommend caching as it's sensitive or private - or at least
230- # select private. don't bother setting Expires as all our
231- # clients are smart enough to be happy with Cache-Control
232- request .setHeader (b"Cache-Control" , b"public,max-age=86400,s-maxage=86400" )
230+ _add_cache_headers (request )
233231
234232 if file_size is not None :
235233 request .setHeader (b"Content-Length" , b"%d" % (file_size ,))
@@ -240,6 +238,26 @@ def _quote(x: str) -> str:
240238 request .setHeader (b"X-Robots-Tag" , "noindex, nofollow, noarchive, noimageindex" )
241239
242240
241+ def _add_cache_headers (request : Request ) -> None :
242+ """Adds the appropriate cache headers to the response"""
243+
244+ # Cache on the client for at least a day.
245+ #
246+ # We set this to "public,s-maxage=0,proxy-revalidate" to allow CDNs to cache
247+ # the media, so long as they "revalidate" the media on every request. By
248+ # revalidate, we mean send the request to Synapse with a `If-None-Match`
249+ # header, to which Synapse can either respond with a 304 if the user is
250+ # authenticated/authorized, or a 401/403 if they're not.
251+ request .setHeader (
252+ b"Cache-Control" , b"public,max-age=86400,s-maxage=0,proxy-revalidate"
253+ )
254+
255+ # Set an ETag header to allow requesters to use it in requests to check if
256+ # the cache is still valid. Since media is immutable (though may be
257+ # deleted), we just set this to a constant.
258+ request .setHeader (b"ETag" , _IMMUTABLE_ETAG )
259+
260+
243261# separators as defined in RFC2616. SP and HT are handled separately.
244262# see _can_encode_filename_as_token.
245263_FILENAME_SEPARATOR_CHARS = {
@@ -336,13 +354,15 @@ def _quote(x: str) -> str:
336354
337355 from synapse .media .media_storage import MultipartFileConsumer
338356
357+ _add_cache_headers (request )
358+
339359 # note that currently the json_object is just {}, this will change when linked media
340360 # is implemented
341361 multipart_consumer = MultipartFileConsumer (
342362 clock ,
343363 request ,
344364 media_type ,
345- {},
365+ {}, # Note: if we change this we need to change the returned ETag.
346366 disposition ,
347367 media_length ,
348368 )
@@ -419,6 +439,46 @@ async def respond_with_responder(
419439 finish_request (request )
420440
421441
442+ def respond_with_304 (request : SynapseRequest ) -> None :
443+ request .setResponseCode (304 )
444+
445+ # could alternatively use request.notifyFinish() and flip a flag when
446+ # the Deferred fires, but since the flag is RIGHT THERE it seems like
447+ # a waste.
448+ if request ._disconnected :
449+ logger .warning (
450+ "Not sending response to request %s, already disconnected." , request
451+ )
452+ return None
453+
454+ _add_cache_headers (request )
455+
456+ request .finish ()
457+
458+
459+ def check_for_cached_entry_and_respond (request : SynapseRequest ) -> bool :
460+ """Check if the request has a conditional header that allows us to return a
461+ 304 Not Modified response, and if it does, return a 304 response.
462+
463+ This handles clients and intermediary proxies caching media.
464+ This method assumes that the user has already been
465+ authorised to request the media.
466+
467+ Returns True if we have responded."""
468+
469+ # We've checked the user has access to the media, so we now check if it
470+ # is a "conditional request" and we can just return a `304 Not Modified`
471+ # response. Since media is immutable (though may be deleted), we just
472+ # check this is the expected constant.
473+ etag = request .getHeader ("If-None-Match" )
474+ if etag == _IMMUTABLE_ETAG :
475+ # Return a `304 Not modified`.
476+ respond_with_304 (request )
477+ return True
478+
479+ return False
480+
481+
422482class Responder (ABC ):
423483 """Represents a response that can be streamed to the requester.
424484
0 commit comments