Coverage for ivatar/middleware.py: 86%
35 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 05:10 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 05:10 +0000
1"""
2Middleware classes
3"""
5import hashlib
6from django.utils.deprecation import MiddlewareMixin
7from django.middleware.locale import LocaleMiddleware
8from django.utils.cache import get_conditional_response
9from django.utils.http import parse_http_date_safe
12class CustomLocaleMiddleware(LocaleMiddleware):
13 """
14 Middleware that extends LocaleMiddleware to skip Vary header processing for image URLs
15 and handle conditional requests (ETag / If-None-Match -> 304 Not Modified).
16 """
18 def process_response(self, request, response):
19 # Check if this is an image-related URL
20 path = request.path
21 if any(
22 path.startswith(prefix)
23 for prefix in [
24 "/avatar/",
25 "/gravatarproxy/",
26 "/blueskyproxy/",
27 "/accounts/raw_image/",
28 ]
29 ):
30 # Preserve existing ETag from view if present, otherwise compute fallback
31 etag = response.get("Etag")
32 if not etag:
33 path_parts = path.strip("/").split("/")
34 if len(path_parts) >= 2:
35 hash_value = path_parts[1] # Get the hash part
36 # Sanitize hash_value to remove newlines and other control characters
37 # that would cause BadHeaderError
38 hash_value = "".join(
39 c for c in hash_value if c.isprintable() and c not in "\r\n"
40 )
41 etag = f'"{hash_value}"'
42 response["Etag"] = etag
43 else:
44 # Fallback to content hash if we can't extract from URL
45 # Use hashlib.md5 for stable hash across processes
46 content_hash = hashlib.md5(response.content).hexdigest()
47 etag = f'"{content_hash}"'
48 response["Etag"] = etag
50 # If response is 200 OK, evaluate conditional request headers (If-None-Match, If-Modified-Since)
51 if response.status_code == 200:
52 last_mod_header = response.get("Last-Modified")
53 last_modified = (
54 parse_http_date_safe(last_mod_header)
55 if last_mod_header
56 else None
57 )
58 conditional_response = get_conditional_response(
59 request,
60 etag=etag,
61 last_modified=last_modified,
62 response=response,
63 )
64 if conditional_response is not response:
65 conditional_response["Etag"] = etag
66 if "Vary" in response:
67 conditional_response["Vary"] = response["Vary"]
68 return conditional_response
70 # Skip the parent's process_response to avoid adding Accept-Language to Vary
71 return response
73 # For all other URLs, use the parent's behavior
74 return super().process_response(request, response)
77class MultipleProxyMiddleware(
78 MiddlewareMixin
79): # pylint: disable=too-few-public-methods
80 """
81 Middleware to rewrite proxy headers for deployments
82 with multiple proxies
83 """
85 def process_request(self, request): # pylint: disable=no-self-use
86 """
87 Rewrites the proxy headers so that forwarded server is
88 used if available.
89 """
90 if "HTTP_X_FORWARDED_SERVER" in request.META:
91 request.META["HTTP_X_FORWARDED_HOST"] = request.META[
92 "HTTP_X_FORWARDED_SERVER"
93 ]