├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── middleware.py └── templatetags.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: pirate 4 | patreon: theSquashSH 5 | custom: https://paypal.me/NicholasSweeting 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nick Sweeting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | ## ⚠️ DEPRECATED now that HTTP3 is released 5 | 6 | #### There are some useful concepts that can be reused from this for HTTP3 and above (e.g. caching and pre-sending hint headers before view code finishes running), but server push support was officially phased out in 2022 in favor of Early Hints, so the this library is now deprecated. 7 | https://developer.chrome.com/blog/removing-push 8 | 9 | --- 10 | 11 |

12 | 13 | # Django HTTP2 Middleware (DEPRECATED) 14 | 15 | --- 16 | 17 |

18 | 19 | ```html 20 | 21 | 22 | ``` 23 | 24 | 25 | 26 | This is a small middlware for Django v2.0+ to automatically generate preload headers from staticfiles used in template rendering, with support for using [`StreamingHttpResponse`](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse) to send cached preload headers in advance of the actual response being generated. The preload headers alone provide large speed boost, but pre-sending the cached headers in advance of view execution is the real advantage that this library provides. 27 | 28 | It's also built to support modern security features like [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) using [`django-csp`](https://django-csp.readthedocs.io/en/latest/configuration.html), it sends `request.csp_nonce` 29 | in preload headers correctly so that preloads aren't rejected by your CSP policy if they require a nonce. Support for automatically generating and attaching CSP hashes for staticfiles and inline blocks is also planned in the near future. 30 | 31 | It's not yet production-ready (I'll put it on PyPI if/when it ever is), but for now it's easily installable by cloning it into your apps folder, and the codebase is small enough to be quickly reviewed and customized to a project's needs. 32 | 33 | --- 34 | 35 | 36 | ## How it works 37 | 38 | It works by providing a templatetag `{% http2static %}` that serves as a drop-in replacement for `{% static %}`, except it records all the urls used while rendering the template in `request.to_preload`. 39 | 40 | The http2 middleware then transforms the list of `to_preload` urls into a full HTTP preload header, which is then attached to the response. When `settings.HTTP2_PRESEND_CACHED_HEADERS = True`, the first response's preload headers will be cached and automatically sent in advance during later requests (using [`StreamingHttpResponse`](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse) to send them before the view executes). Upstream servers like Nginx and CloudFlare can then use these headers to do HTTP2 server push, delivering the resources to clients before they are requested during browser parse & rendering. With [TCP fast-open](https://en.wikipedia.org/wiki/TCP_Fast_Open), [TLS 1.3](https://blog.cloudflare.com/rfc-8446-aka-tls-1-3/), and [HTTP2 server push](https://www.smashingmagazine.com/2017/04/guide-http2-server-push/), it's now possible to have entire pageloads with only 1 round-trip, now all we need are cache-digests and QUIC and then we'll be at web nirvana 🎂. 41 | 42 | 43 | 44 | ### HTTP2 server-push 45 | 46 | When preload headers are sent fast and `HTTP2_SERVER_PUSH = True` is enabled in `settings.py`, upstream servers like Nginx or Cloudflare HTTP2 will usually finish server pushing all the page resources not only before the browser requests them, but even before the view is finished executing, providing a 100ms+ headstart to static file loading in some cases. When enabled it's very cool to look at the network waterfall visualization and see your page's statcifiles finish loading together, a full 50ms+ before the HTML is even returned from Django! 47 | 48 | Unfortunately, while shiny and exciting, this wont necessarily make your site faster for real-world users. In fact, it can sometimes make sites slower because after the first visit, users have most of the resources cached anyway, and pushing uneeded files on every request can waste network bandwidth and use IO & CPU capacity that otherwise would've gone towards loading the actual content. You should toggle the config options while testing your project to see if server push provides real-world speed gains, or use the [recommended settings](#Recommended-Settings) listed below that provide speed gains in most cases without the risk of wasting bandwidth to push uneeded resources. There are some cases where HTTP2 push is still worth it though, e.g. if you have to push a small bundle of static files for first paint and most of your users are first-time visitors without your site cached. 49 | 50 | HTTP2 server-push will eventually become the optimal method of page delivery once cache-digests are released (improving both latency and bandwidth use). Read these articles and the links within them to learn more about HTTP2, server push, and why cache digests are an important feature needed to make server-push worth it: 51 | 52 | - https://http2.github.io/faq/#whats-the-benefit-of-server-push 53 | - https://calendar.perfplanet.com/2016/cache-digests-http2-server-push/ 54 | - https://httpwg.org/http-extensions/cache-digest.html#introduction 55 | 56 | This library is still useful without server push enabled though, as it's primary function is to collect statifiles and send them as `` preload headers in parallel *before the Django views finish executing*, which can provide a 100ms+ headstart for the browser to start loading page content in many cases. The optimal recommended settings for maximum speed gain (as of 2019/07) are to send preload headers, cache them and send them in advance, but don't enable `HTTP2_SERVER_PUSH` until cache-digest functionality is released in most browsers. 57 | 58 | ## Install: 59 | 60 | 1. Clone this repo as into your project folder next to `manage.py` as a new django app called "http2": 61 | ```bash 62 | cd /opt/your-project/project-django/ 63 | git clone https://github.com/pirate/django-http2-middleware http2 64 | ``` 65 | 66 | 2. Add `http2.middleware.HTTP2Middleware` to your `MIDDLEWARE` list in `settings.py`: 67 | ```python 68 | MIDDLEWARE = [ 69 | ... 70 | 'csp.middleware.CSPMiddleware', # (optional if you use django-csp, it must be above the http2 middleware) 71 | 'http2.middleware.HTTP2Middleware', # (add the middleware at the end, but before gzip) 72 | ] 73 | # (adding "http2" to INSTALLED_APPS is not needed) 74 | ``` 75 | 76 | 3. Add the required configuration options to your `settings.py`: 77 | ```python 78 | HTTP2_PRELOAD_HEADERS = True 79 | HTTP2_PRESEND_CACHED_HEADERS = True 80 | HTTP2_SERVER_PUSH = False 81 | ``` 82 | 83 | 4. (Optional) Add the templatag as a global template builtin in `settings.py`: 84 | This will make `{% http2static %}` availabe in templates without needing `{% load http2 %}` at the top. 85 | ```python 86 | TEMPLATES = [ 87 | { 88 | ... 89 | 'OPTIONS': { 90 | ... 91 | 'builtins': [ 92 | ... 93 | 'http2.templatetags', 94 | ], 95 | }, 96 | }, 97 | ... 98 | ] 99 | ``` 100 | 101 | 5. (Optional if using `django-csp`) Include nonces on any desired resource types in `settings.py`: 102 | Generated preload headers will automatically include this nonce using `{{request.csp_nonce}}`. 103 | ```python 104 | # add any types you want to use with nonce-validation (or just add it to the fallback default-src) 105 | CSP_DEFAULT_SRC = ("'self'", ...) 106 | CSP_INCLUDE_NONCE_IN = ('default-src', ...) 107 | ``` 108 | 109 | ## Usage 110 | 111 | Just use the `{% http2static '...' %}` tag instead of `{% static '...' %}` anytime you want to have a resource preloaded. 112 | 113 | ```html 114 | 115 | 116 | 117 | 118 | ... 119 | 120 | 121 | 122 | ``` 123 | 124 | Don't use `{% http2static %}` for everything, just use it for things in the critical render path that are needed for the initial pageload. It's best used for CSS, JS, fonts, and icons required to render the page nicely, but usually shouldn't be used for non-critical footer scripts and styles, async page content, images, video, audio, or other media. 125 | 126 | 127 | ## Configuration 128 | 129 | ### Recommended Settings 130 | 131 | These settings provide the most speed gains for 90% of sites, though it's worth testing all the possibilities to see the real-world results for your project. 132 | 133 | ```python 134 | HTTP2_PRELOAD_HEADERS = True 135 | HTTP2_PRESEND_CACHED_HEADERS = True 136 | HTTP2_SERVER_PUSH = False 137 | ``` 138 | ### `django-http2-middleware` Configuration 139 | 140 | #### `HTTP2_PRELOAD_HEADERS` 141 | *Values:* [`True`]/`False` 142 | 143 | Attach any `{% http2static %}` urls used templates in an auto-generated HTTP preload header on the response. 144 | Disable this to turn off preload headers and disable the middleware entirely, this also prevents both header caching and http2 server push. 145 | 146 | #### `HTTP2_PRESEND_CACHED_HEADERS` 147 | *Values:* [`True`]/`False` 148 | 149 | Cache first request's preload urls and send in advance on subsequent requests. 150 | Eanble this to cache the first request's generated preload headers and use [`StreamingHttpResponse`](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse) on subsequent requests to send the headers early before the view starts executing. Disable this to use normal HTTPResponses with the preload headers attached at the end of view execution. 151 | 152 | #### `HTTP2_SERVER_PUSH` 153 | *Values:* `True`/[`False`] 154 | 155 | Allow upstream servers to server-push any files in preload headers. 156 | Disable this to add `; nopush` to all the preload headers to prevent upstream servers from pushing resources in advance. 157 | Keeping this set to `False` is recommended until cache-digests are sent by most browsers. 158 | 159 | ### `django-csp` Configuration 160 | 161 | There are many ways to implement Content Security Policy headers and nonces with Django, 162 | the most popular for django is [`django-csp`](https://github.com/mozilla/django-csp), 163 | which is library maintained by Mozilla. This library is built to be compatible 164 | with Mozilla's `django-csp`, but it's not required to use both together. You can find more info about 165 | configuring Django to do CSP verification here: 166 | 167 | - https://django-csp.readthedocs.io/en/latest/configuration.html#policy-settings 168 | - https://content-security-policy.com/ 169 | - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 170 | 171 | ### Webserver Configuration 172 | 173 | In order to use HTTP2 server push, you need a webserver in front of Django that reads 174 | the preload headers and pushes the files. Cloudflare has a GUI [control panel option](https://www.cloudflare.com/website-optimization/http2/serverpush/) to enable server push, 175 | and nginx can do it with only one extra line of config: 176 | 177 | ```nginx 178 | server { 179 | listen 443 ssl http2; 180 | http2_push_preload on; # nginx will automatically server-push anything specified in preload headers 181 | ... 182 | } 183 | ``` 184 | 185 | See more info and nginx http2 options here: 186 | 187 | - https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/ 188 | - http://nginx.org/en/docs/http/ngx_http_v2_module.html 189 | 190 | 191 | ## Verifying it works 192 | 193 | 194 | 195 | Responses can be served in three different ways when using `django-http2-middleware`. You can inspect which way is 196 | used for a given response by looking at the `x-http2-preload` header attached to the response. 197 | If all the options are enabled, it takes two initial requests after enabling the middleware and starting Django for the cache to warm up, one to detect the content type, and one to build the list of resource URLs used by the template: 198 | 199 | 1. The first request to a given URL has no preload headers sent in advance (`x-http2-preload: off`). It's used to confirm that the request and response are `Content-Type: text/html` and not a JSON API request, file download, or other non-html type that shouldn't have preload headers attached. 200 | 2. The second request has preload headers but only attaches them after the response is generated (`x-http2-preload: late`). It's used build the initial cache of preload urls for the given `request.path` by collecting urls used by `{% http2static %}` tags during template rendering. 201 | 3. If `HTTP2_PRESEND_CACHED_HEADERS = True`, the third request (and all requests after that) send the cached headers immediately before the response is generated (`x-http2-preload: early`). If presending cached headers is disabled, then `StreamingHttpResponse` wont be used to pre-send headers before the view, and preload headers will be attached after the response as usual in `x-http2-preload: late` mode. 202 | 203 | Start runserver behind nginx and reload your page 4 times while watching the dev console to confirm the cache warms up properly and later requests receive server-pushed resources. If everyting is working correctly, 204 | the third pageload and all subsequent loads by all users should show up with the `x-http2-preload: early` response header, and pushed resources should appear significantly earlier in the network timing watefall view. 205 | 206 | You can inspect the preload performance of a given page and confirm it matches what you expect for its `x-http2-preload` mode using the network requests waterfall graph in the Chrome/Firefox/Safari dev tools. 207 | 208 | 209 | | `x-http2-preload: off` | `x-http2-preload: late` | `x-http2-preload: early` | 210 | | ------------------------------ | -------------------------------------- | ------------------------------------- | 211 | | ![](https://i.imgur.com/sN5Rmjn.png) | ![](https://i.imgur.com/pSOcGQy.png) | ![](https://i.imgur.com/ouRu1rf.png) | 212 | | Requires: | Requires: | Requires: | 213 | | `HTTP2_PRELOAD_HEADERS = True` | `HTTP2_PRELOAD_HEADERS = True` | `HTTP2_PRELOAD_HEADERS = True` | 214 | | | `HTTP2_PRESEND_CACHED_HEADERS = True` | `HTTP2_PRESEND_CACHED_HEADERS = True` | 215 | | | | `HTTP2_SERVER_PUSH = True` | 216 | 217 | If you set `HTTP2_PRESEND_CACHED_HEADERS = True` and `HTTP2_SERVER_PUSH = False`, responses will all be sent in `x-http2-preload: late` mode, which is the recommended mode until cache digests become available in most browsers. 218 | 219 |
220 | 221 | 222 | 223 |
224 | 225 | ## Further Reading 226 | 227 | ### Docs & Articles 228 | 229 | - https://dexecure.com/blog/http2-push-vs-http-preload/ 230 | - https://www.keycdn.com/blog/http-preload-vs-http2-push 231 | - https://symfony.com/doc/current/web_link.html 232 | - https://www.smashingmagazine.com/2017/04/guide-http2-server-push/ 233 | - http2.github.io/faq/#whats-the-benefit-of-server-push 234 | - https://calendar.perfplanet.com/2016/cache-digests-http2-server-push 235 | - https://httpwg.org/http-extensions/cache-digest.html#introduction 236 | - https://django-csp.readthedocs.io/en/latest/configuration.html#policy-settings 237 | - https://content-security-policy.com 238 | - htts://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 239 | 240 | ### Similar Projects 241 | 242 | After making my own solution I discovered great minds think alike, and a few people have done exactly the same thing before me already! 243 | It's crazy how similarly we all chose to implement this, everyone used a drop-in replacement for `{% static %}`, I guess it goes to show 244 | that Django is particularly designed well in this area, because there's one obvious way to do things and everyone independently figured it out and implemented robust solutions in <200LOC. 245 | 246 | - https://github.com/ricardochaves/django_http2_push 247 | - https://github.com/fladi/django-static-push 248 | - https://github.com/DistPub/nginx-http2-django-server-push 249 | 250 | However, none of these support CSP policies (which require adding nonces to the preload headers), or use [`StreamingHttpResponse`](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse) 251 | to send push headers before the view executes, so in some ways this project takes adventage of the available HTTP2 speed-up methods to the fullest degree out of the 4. 252 | 253 | ### Project Status 254 | 255 | Consider this library "beta" software, still rough in some areas, but used in production for 6+ months on several projects. It's not on PyPi tet, I'll publish it once it's nicer and has more tests. For now it should be cloned into your Django folder, or used piecewise as inspiration for your own code. 256 | 257 | Once HTTP2 [cache digests](https://httpwg.org/http-extensions/cache-digest.html) are finalized, server push will ~~invariably~~(2020 Edit: [lol](https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/vOWBKZGoAQAJ)) become the fastest way to deliver assets, and this project will get more of my time as we integrate it into all our production projects at @Monadical-SAS. To read more about why cache digests are critical to HTTP2 server push actually being useful, this article is a great resource: 258 | 259 |
260 | 261 |
262 | 263 | ["Cache Digests: Solving the Cache Invalidation Problem of HTTP/2 Server Push to Reduce Latency and Bandwidth"](https://calendar.perfplanet.com/2016/cache-digests-http2-server-push/) by Sebastiaan Deckers 264 | 265 |
266 | 267 | ## Bonus Material 268 | 269 | Did you know you can run code *after a Django view returns a response* without using Celery, Dramatiq, or another background worker system? 270 | Turns out it's trivially easy, but very few people know about it. 271 | 272 | ```python 273 | def my_view(request): 274 | ... 275 | return HttpResponseWithCallback(..., callback=some_expensive_function) 276 | 277 | class HttpResponseWithCallback(HttpResponse): 278 | def __init__(self, *args, **kwargs): 279 | self.callback = kwargs.pop('callback', None) 280 | super().__init__(*args, **kwargs) 281 | 282 | def close(self): 283 | super().close() 284 | self.callback and self.callback(response=self) 285 | ``` 286 | 287 | 288 | In small projects it's perfect for sending signup emails, tracking analytics events, writing to files, or any other CPU/IO intensive task that you don't want to block the user on. 289 | In large projects, this is an antipattern because it encourages putting big blocking IO or CPU operations in these "fake" async request callbacks. The callbacks don't actually run asyncronously (like Celery), they don't provide any free performance improvement on the main server thread, in they just hide some operations outside of the normal request/response lifecycle and make it hard to track down latency issues. You probably don't want to block main Django worker threads with things that would be better handled in the background, as it'll greatly reduce the number of simultaneous users your servers can handle. 290 | 291 | For a full example demonstrating this library and more, check out this gist: [django_turbo_response.py](https://gist.github.com/pirate/79f84dfee81ba0a38b6113541e827fd5). 292 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirate/django-http2-middleware/7a065261256774155c16a3e27e23145b0e9dee3c/__init__.py -------------------------------------------------------------------------------- /middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import StreamingHttpResponse 3 | 4 | 5 | PRELOAD_AS = { 6 | 'js': 'script', 7 | 'css': 'style', 8 | 'png': 'image', 9 | 'jpg': 'image', 10 | 'jpeg': 'image', 11 | 'webp': 'image', 12 | 'svg': 'image', 13 | 'gif': 'image', 14 | 'ttf': 'font', 15 | 'woff': 'font', 16 | 'woff2': 'font' 17 | } 18 | PRELOAD_ORDER = { 19 | 'css': 0, 20 | 'ttf': 1, 21 | 'woff': 1, 22 | 'woff2': 1, 23 | 'js': 2, 24 | } 25 | 26 | 27 | cached_preload_urls = {} 28 | cached_response_types = {} 29 | 30 | 31 | 32 | def record_file_to_preload(request, url): 33 | """save a staticfile to the list of files to push via HTTP2 preload""" 34 | if not hasattr(request, 'to_preload'): 35 | request.to_preload = set() 36 | 37 | request.to_preload.add(url) 38 | 39 | 40 | def create_preload_header(urls, nonce=None, server_push=None): 41 | """Compose the Link: header contents from a list of urls""" 42 | without_vers = lambda url: url.split('?', 1)[0] 43 | extension = lambda url: url.rsplit('.', 1)[-1].lower() 44 | preload_priority = lambda url: PRELOAD_ORDER.get(url[1], 100) 45 | 46 | urls_with_ext = ((url, extension(without_vers(url))) for url in urls) 47 | sorted_urls = sorted(urls_with_ext, key=preload_priority) 48 | 49 | nonce = f'; nonce={nonce}' if nonce else '' 50 | if server_push is None: 51 | server_push = getattr(settings, 'HTTP2_SERVER_PUSH', False) 52 | 53 | nopush = '' if server_push else '; nopush' 54 | 55 | preload_tags = ( 56 | f'<{url}>; rel=preload; crossorigin; as={PRELOAD_AS[ext]}{nonce}{nopush}' 57 | if ext in PRELOAD_AS else 58 | f'<{url}>; rel=preload; crossorigin{nonce}{nopush}' 59 | for url, ext in sorted_urls 60 | ) 61 | return ', '.join(preload_tags) 62 | 63 | 64 | def get_cached_response_type(request): 65 | global cached_response_types 66 | return cached_response_types.get(request.path, '') 67 | 68 | def set_cached_response_type(request, response): 69 | global cached_response_types 70 | cached_response_types[request.path] = response['Content-Type'].split(';', 1)[0] 71 | 72 | def get_cached_preload_urls(request): 73 | global cached_preload_urls 74 | 75 | 76 | 77 | if (getattr(settings, 'HTTP2_PRESEND_CACHED_HEADERS', False) 78 | and request.path in cached_preload_urls): 79 | 80 | return cached_preload_urls[request.path] 81 | 82 | return () 83 | 84 | def set_cached_preload_urls(request): 85 | global cached_preload_urls 86 | 87 | if (getattr(settings, 'HTTP2_PRESEND_CACHED_HEADERS', False) 88 | and getattr(request, 'to_preload', None)): 89 | 90 | cached_preload_urls[request.path] = request.to_preload 91 | 92 | 93 | def should_preload(request): 94 | request_type = request.META.get('HTTP_ACCEPT', '')[:36] 95 | cached_response_type = get_cached_response_type(request) 96 | # print('REQUEST TYPE', request_type) 97 | # print('CACHED RESPONSE TYPE', cached_response_type) 98 | return ( 99 | getattr(settings, 'HTTP2_PRELOAD_HEADERS', False) 100 | and 'text/html' in request_type 101 | and 'text/html' in cached_response_type 102 | ) 103 | 104 | def early_preload_response(request, get_response, nonce): 105 | def generate_response(): 106 | yield '' 107 | response = get_response(request) 108 | set_cached_response_type(request, response) 109 | yield response.content 110 | 111 | response = StreamingHttpResponse(generate_response()) 112 | response['Link'] = create_preload_header(request.to_preload, nonce) 113 | response['X-HTTP2-PRELOAD'] = 'early' 114 | 115 | # print('SENDING EARLY PRELOAD REQUEST', request.path, response['Content-Type']) 116 | return response 117 | 118 | def late_preload_response(request, get_response, nonce): 119 | response = get_response(request) 120 | set_cached_response_type(request, response) 121 | 122 | if getattr(request, 'to_preload'): 123 | preload_header = create_preload_header(request.to_preload, nonce) 124 | response['Link'] = preload_header 125 | set_cached_preload_urls(request) 126 | response['X-HTTP2-PRELOAD'] = 'late' 127 | 128 | # print('SENDING LATE PRELOAD REQUEST', request.path, response['Content-Type']) 129 | return response 130 | 131 | def preload_response(request, get_response): 132 | nonce = getattr(request, 'csp_nonce', None) 133 | cached_preload_urls = get_cached_preload_urls(request) 134 | if cached_preload_urls: 135 | request.to_preload = cached_preload_urls 136 | return early_preload_response(request, get_response, nonce) 137 | 138 | return late_preload_response(request, get_response, nonce) 139 | 140 | def no_preload_response(request, get_response): 141 | response = get_response(request) 142 | set_cached_response_type(request, response) 143 | # print('SENDING NO PRELOAD REQUEST', request.path, response['Content-Type']) 144 | response['X-HTTP2-PRELOAD'] = 'off' 145 | return response 146 | 147 | 148 | def HTTP2Middleware(get_response): 149 | def middleware(request): 150 | """Attach a Link: header containing preload links for every staticfile 151 | referenced during the request by the {% http2static %} templatetag 152 | """ 153 | if should_preload(request): 154 | return preload_response(request, get_response) 155 | return no_preload_response(request, get_response) 156 | return middleware 157 | -------------------------------------------------------------------------------- /templatetags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.staticfiles.templatetags.staticfiles import static 3 | 4 | from .middleware import record_file_to_preload 5 | 6 | register = template.Library() 7 | 8 | @register.simple_tag(takes_context=True) 9 | def http2static(context: dict, path: str, version: str=None) -> str: 10 | """ 11 | same as static templatetag, except it saves the list of files used 12 | to request.to_preload in order to push them up to the user 13 | before they request it using HTTP2 push via the HTTP2PushMiddleware 14 | """ 15 | url = f'{static(path)}?v={version}' if version else static(path) 16 | record_file_to_preload(context['request'], url) 17 | return url 18 | --------------------------------------------------------------------------------