Skip to content

Instantly share code, notes, and snippets.

@pirate
Last active November 21, 2022 21:12
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pirate/79f84dfee81ba0a38b6113541e827fd5 to your computer and use it in GitHub Desktop.
Save pirate/79f84dfee81ba0a38b6113541e827fd5 to your computer and use it in GitHub Desktop.
An extended HTTPResponse class for Django 2.2 adding support for streaming partial template responses incrementally, preload headers, HTTP2 server push, CSP headers, running post-request callbacks, and more (fully typed).
"""
This is an extended HTTP response class for Django >=2.0 that adds lots of fancy features.
It's most useful when needing to accellerate slow view functions that return templates,
but it can be used anywhere where you need to return an HTTPResponse() or render(...).
It works by subclassing the builtin Django HttpResponse and StreamingHttpResponse,
and adding improvements in many areas, including functionality, speed, and security.
The most up-to-date version of this code can be found here:
https://gist.github.com/pirate/79f84dfee81ba0a38b6113541e827fd5
Features:
- can stream response fragments incrementally from a generator in the view
- pre-sending response headers before the view code starts executing (can speed up pageloads dramatically)
- splitting up templates into a head, body, and footer, and sending the head section before the view starts (can speed up pageloads dramatically)
- adding HTTP preload headers (using django-http2-middleware)
- enabling HTTP2 server-push (using django-http2-middleware)
- automatically including any needed CSP headers with nonces (using django-csp)
- running async callbacks after the response is sent to the user without using signals or needing jobs systems like Celery/dramatiq
Dependencies:
- (required) django-http2-middleware https://github.com/pirate/django-http2-middleware
- (optional) django-csp https://github.com/mozilla/django-csp
Usage:
class ExampleView(View):
def get(self, request):
def render_body():
yield '<span> some html here</span>'
return TurboStreamingResponse(request, render_body)
Further reading:
- https://github.com/pirate/django-http2-middleware
- https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse
- https://andrewbrookins.com/django/how-does-djangos-streaminghttpresponse-work-exactly/
- https://dexecure.com/blog/http2-push-vs-http-preload
- https://github.com/mozilla/django-csp
"""
import sys
from typing import Dict, List, Iterable, Optional, Callable, Any
from http2 import create_preload_header # see https://github.com/pirate/django-http2-middleware
from django.conf import settings
from django.urls import resolve
from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
from django.template.loader import render_to_string
from django.contrib.auth.models import User
### Types
class RequestType(HttpRequest):
user: User
ResponseType = HttpResponse
HeadersType = Dict[str, str]
PreloadsType = List[str]
ContextType = Dict[str, Any]
RenderContent = Iterable[str]
RenderFunction = Callable[[], RenderContent]
CallbackFunction = Callable[[ResponseType], None]
def current_function_name(above=1):
"""returns the calling function's __module__.__name__"""
# https://gist.github.com/JettJones/c236494013f22723c1822126df944b12#gistcomment-2962311
frame = sys._getframe()
for frame_idx in range(0, above):
frame = frame.f_back
caller_module = frame.f_globals["__name__"]
caller_name = frame.co_code.co_name
return f'{caller_module}.{caller_name}'
### Base Classes
class BaseTurboResponse(ResponseType):
# Instance Attribute Types
_request: RequestType
_render: RenderFunction
_callback: CallbackFunction
_headers: HeadersType
_preloads: PreloadsType
_enable_preload: bool
_enable_push: bool
def __init__(self,
request: RequestType,
render: RenderFunction,
headers: Optional[HeadersType]= None,
preloads: Optional[PreloadsType]=None,
callback: Optional[CallbackFunction]=None,
enable_preload: bool=getattr(settings, 'HTTP2_PRELOAD_HEADERS', True),
enable_push: bool=getattr(settings, 'HTTP2_SERVER_PUSH', True),
**kwargs):
self._request = request
self._response = render # type: ignore
self._headers = headers or {}
self._preloads = preloads or []
self._callback = callback or (lambda _: None) # type: ignore
self._enable_preload = enable_preload
self._enable_push = enable_push
super().__init__(render(), **kwargs)
self._set_headers()
self._set_preloads()
def _set_preloads(self):
if self._preloads and self._enable_preload:
self['Link'] = create_preload_header(
urls=self._preloads,
csp_nonce=getattr(self._request, 'csp_nonce', None),
server_push=self._enable_push,
)
def _set_headers(self):
if self._headers:
for name, value in self._headers.items():
self[name] = value
def close(self):
self._callback(response=self)
super().close()
class BaseTurboStreamingResponse(BaseTurboResponse, StreamingHttpResponse):
pass
### Main Classes
class TurboResponse(BaseTurboResponse):
BASE_HEADERS = getattr(settings, 'BASE_HEADERS', {})
BASE_PRELOADS = getattr(settings, 'BASE_PRELOADS', {})
BASE_CONTEXT = getattr(settings, 'BASE_CONTEXT', {})
BASE_TEMPLATE_HEAD = getattr(settings, 'BASE_TEMPLATE_HEAD', None)
BASE_TEMPLATE_BODY = getattr(settings, 'BASE_TEMPLATE_BODY', None)
BASE_TEMPLATE_FOOT = getattr(settings, 'BASE_TEMPLATE_FOOT', None)
def __init__(self,
request: RequestType,
render_body: Optional[RenderFunction]=None,
render_head: Optional[RenderFunction]=None,
render_foot: Optional[RenderFunction]=None,
render: Optional[RenderFunction]=None,
head_template: Optional[str]=None,
body_template: Optional[str]=None,
foot_template: Optional[str]=None,
context: Optional[ContextType]=None,
extra_context: Optional[ContextType]=None,
headers: Optional[HeadersType]= None,
extra_headers: Optional[HeadersType]= None,
preloads: Optional[PreloadsType]=None,
extra_preloads: Optional[PreloadsType]=None,
callback: Optional[CallbackFunction]=None,
**kwargs):
self.BASE_HEADERS = {**self.BASE_HEADERS}
self.BASE_PRELOADS = {**self.BASE_PRELOADS}
self.BASE_CONTEXT = {
**self.BASE_CONTEXT,
'REQUEST_VIEW_NAME': current_function_name(2),
'CSP_NONCE': getattr(request, 'csp_nonce', None),
}
super().__init__(
request=request,
render=self._build_render(
request,
render,
render_head,
render_body,
render_foot,
head_template,
body_template,
foot_template,
context,
extra_context,
),
headers=self._build_headers(headers, extra_headers),
preloads=self._build_preloads(preloads, extra_preloads),
callback=callback,
**kwargs,
)
@classmethod
def get_context(cls, request):
return {
**cls.BASE_CONTEXT,
'REQUEST_VIEW_NAME': current_function_name(2),
'CSP_NONCE': getattr(request, 'csp_nonce', None),
}
def _build_headers(self,
headers: Optional[HeadersType],
extra_headers: Optional[HeadersType]) -> HeadersType:
extra_headers = extra_headers or {}
if headers is not None:
return {**headers, **extra_headers}
return {**self.BASE_HEADERS, **extra_headers}
def _build_preloads(self,
preloads: Optional[PreloadsType],
extra_preloads: Optional[PreloadsType]) -> PreloadsType:
extra_preloads = extra_preloads or []
if preloads is not None:
return [*preloads, *extra_preloads]
return [*self.BASE_PRELOADS, *extra_preloads]
def _build_render(self,
request: RequestType,
render: Optional[RenderFunction],
render_head: Optional[RenderFunction],
render_body: Optional[RenderFunction],
render_foot: Optional[RenderFunction],
head_template: Optional[str],
body_template: Optional[str],
foot_template: Optional[str],
context: Optional[ContextType],
extra_context: Optional[ContextType]) -> RenderFunction:
if render:
return render
extra_context = extra_context or {}
if context is not None:
context = {**context, **extra_context}
else:
context = {**self.BASE_CONTEXT, **extra_context}
def combined_render() -> RenderContent:
if not (render_head or head_template or self.BASE_TEMPLATE_HEAD):
raise ValueError('Missing render_head or head_template argument (and no default was defined in settings.BASE_TEMPLATE_HEAD)')
if not (render_body or body_template or self.BASE_TEMPLATE_BODY):
raise ValueError('Missing render_body or body_template argument (and no default was defined in settings.BASE_TEMPLATE_BODY)')
if not (render_foot or foot_template or self.BASE_TEMPLATE_FOOT):
raise ValueError('Missing render_foot or foot_template argument (and no default was defined in settings.BASE_TEMPLATE_FOOT)')
# yield immediately to force response.write to send headers early
yield ''
# then incrementally render and yield the head, body, and foot segments
if render_head:
yield from render_head()
else:
yield render_to_string(
head_template or self.BASE_TEMPLATE_HEAD,
context=context,
request=request,
)
if render_body:
yield from render_body()
else:
yield render_to_string(
body_template or self.BASE_TEMPLATE_BODY,
context=context,
request=request,
)
if render_foot:
yield from render_foot()
else:
yield render_to_string(
foot_template or self.BASE_TEMPLATE_FOOT,
context=context,
request=request,
)
return combined_render
class TurboStreamingResponse(BaseTurboStreamingResponse, TurboResponse):
pass
############################## Full Example Usage ##############################
# import sys
# import traceback
# from django.views import View
# from django.shortcuts import redirect
# from django.contrib.auth.mixins import LoginRequiredMixin
# from http2 import get_preloads_from_template
#
#
# ### Helpers for the example code
#
# class ExampleSettings:
# DEBUG = True
# GIT_SHA = 'e8e83f5'
# HOSTNAME = 'example.local'
# ENV = 'DEV'
# BASE_URL = 'https://example.local:8000'
# HTTP2_PRELOAD_HEADERS = True
# HTTP2_SERVER_PUSH = True
# BASE_TEMPLATE = 'base.html'
# BASE_TEMPLATE_HEAD = 'base_head.html'
# BASE_TEMPLATE_BODY = 'base_body.html'
# BASE_TEMPLATE_FOOT = 'base_foot.html'
# BASE_HEADERS: HeadersType = {
# 'X-GIT-SHA': GIT_SHA,
# 'X-DEBUG': str(DEBUG),
# }
# BASE_PRELOADS: PreloadsType = [
# *get_preloads_from_template(BASE_TEMPLATE),
# # can add more preloads used on every page here:
# # 'css/base.css',
# # 'js/base.js',
# ]
# BASE_CONTEXT: ContextType = {
# 'HOSTNAME': HOSTNAME,
# 'GIT_SHA': GIT_SHA,
# 'DEBUG': DEBUG,
# 'ENV': ENV,
# 'BASE_URL': BASE_URL,
# }
#
# settings = ExampleSettings()
#
#
#
# ### Example Class-Based View Usage
#
# class SomeExpensiveView(LoginRequiredMixin, View):
# template = 'ui/example.html' # doesn't extend base.html, just contains <main>... content ...</main>
#
# def get(self, request: RequestType) -> Optional[ResponseType]:
# org = request.user.org
# if org.is_disabled:
# return redirect(f'/admin/core/organization/{org.id}/change/')
#
# def render_body() -> RenderContent:
# context = {
# **TurboStreamingResponse.get_context(request),
# 'users': {u.id: u.__json__() for u in User.objects.all()},
# }
# yield render_to_string(self.template, context, request=request)
#
# expensive_sum = sum(
# u.relation_set.filter(some_really__hard__query='abc').count()
# for u in context['users'].values()
# )
# yield f'<div class="footer-info">Total count: {expensive_sum}</div>'
#
# return TurboStreamingResponse(request, render_body)
#
#
# ### Example Function-Based View Usage
#
# def some_expensive_view(request: RequestType) -> ResponseType:
# template = 'ui/example.html'
#
# org = request.user.org
# if org.is_disabled:
# return redirect(f'/admin/core/organization/{org.id}/change/')
#
# def render_body() -> RenderContent:
# body_context = {
# **TurboStreamingResponse.get_context(request),
# 'users': {u.id: u.__json__() for u in User.objects.all()},
# }
# yield render_to_string(template, body_context, request=request)
#
# expensive_sum = sum(
# u.relation_set.filter(some_really__hard__query='abc').count()
# for u in body_context['users'].values()
# )
# yield f'<div class="footer-info">Total count: {expensive_sum}</div>'
#
# return TurboStreamingResponse(request, render_body)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment