HTTP Responses
Buraq provides Django-compatible HTTP response classes in buraq.http.
from buraq.http import ( HttpResponse, JsonResponse, Http404, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseForbidden, HttpResponseNotFound, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseNotModified, HttpResponseGone, HttpResponseServerError, StreamingHttpResponse,)HttpResponse
Section titled “HttpResponse”The base response class — like Django’s HttpResponse.
from buraq.http import HttpResponse
async def my_view(request): return HttpResponse("<h1>Hello</h1>")
# Custom status and content typeasync def plain_text(request): return HttpResponse("Plain text", content_type="text/plain", status=200)
# Binary contentasync def download(request): return HttpResponse(b"\x89PNG...", content_type="application/octet-stream")Setting headers
Section titled “Setting headers”response = HttpResponse("OK")response["X-Custom-Header"] = "value"response["Cache-Control"] = "no-cache"
# Checkif response.has_header("X-Custom-Header"): del response["X-Custom-Header"]Cookies
Section titled “Cookies”response = HttpResponse("OK")response.set_cookie("session_id", "abc123", httponly=True, secure=True, max_age=3600)response.delete_cookie("old_cookie")JsonResponse
Section titled “JsonResponse”Returns a JSON-encoded response. Uses orjson (Rust-based) for serialization — significantly faster than Python’s stdlib json.
from buraq.http import JsonResponse
async def api_view(request): return JsonResponse({"status": "ok", "count": 42})
# Lists, primitives, etc. work by default (safe=False is the default)async def list_view(request): return JsonResponse([1, 2, 3])
# Restrict to dicts only with safe=Trueasync def strict_view(request): return JsonResponse({"status": "ok"}, safe=True)
# Custom statusasync def error_view(request): return JsonResponse({"error": "not found"}, status=404)orjson options
Section titled “orjson options”Pass json_opts to use orjson’s extra serialization options:
import orjsonfrom buraq.http import JsonResponse
# Sort keys, pretty-printreturn JsonResponse(data, json_opts=orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2)
# Serialize numpy arrays, UUIDs, datetimes nativelyreturn JsonResponse(data, json_opts=orjson.OPT_NON_STR_KEYS)StreamingHttpResponse
Section titled “StreamingHttpResponse”For large responses that should be streamed to the client without buffering — like CSV exports or large file downloads.
from buraq.http import StreamingHttpResponse
async def csv_export(request): async def generate(): yield b"id,name,email\n" async for user in User.objects.all(): yield f"{user.id},{user.name},{user.email}\n".encode()
response = StreamingHttpResponse(generate(), content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="users.csv"' return responseFileResponse
Section titled “FileResponse”Serve a file from disk with the correct Content-Type and Content-Disposition.
from buraq.http import FileResponse
# Download attachment (default)async def download_report(request): return FileResponse("/reports/monthly.pdf")
# Inline display (opens in browser)async def preview_image(request, pk: int): doc = await Document.objects.get(id=pk) return FileResponse(doc.path, as_attachment=False)
# Custom download filenameasync def export_csv(request): return FileResponse("/tmp/export.csv", filename="users.csv")as_attachment=True (default) sets Content-Disposition: attachment so the browser downloads the file. Set as_attachment=False for inline rendering (e.g. PDFs, images).
The Content-Type is guessed from the filename using Python’s mimetypes module; pass content_type explicitly to override:
FileResponse("/data/blob", content_type="application/octet-stream")Http404
Section titled “Http404”Raise Http404 inside any view to return a 404 response. Buraq registers an exception handler for it automatically — no extra setup needed.
from buraq.http import Http404
async def post_detail(request, pk: int): post = await Post.objects.get_or_none(id=pk) if post is None: raise Http404(f"Post {pk} does not exist") return await render(request, "post.html", {"post": post})Redirect Responses
Section titled “Redirect Responses”from buraq.http import HttpResponseRedirect, HttpResponsePermanentRedirect
# 302 temporary redirectreturn HttpResponseRedirect("/new-url")
# 301 permanent redirectreturn HttpResponsePermanentRedirect("/permanent-url")Error Responses
Section titled “Error Responses”| Class | Status | Use for |
|---|---|---|
HttpResponseBadRequest |
400 | Invalid client input |
HttpResponseForbidden |
403 | Permission denied |
HttpResponseNotFound |
404 | Resource not found |
HttpResponseNotAllowed |
405 | Wrong HTTP method |
HttpResponseGone |
410 | Resource permanently removed |
HttpResponseServerError |
500 | Internal error |
HttpResponseNotModified |
304 | Cache hit, no body sent |
from buraq.http import ( HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseNotModified,)
async def protected_view(request): if not request.user.is_admin: return HttpResponseForbidden("Admin only")
async def post_only(request): if request.method != "POST": return HttpResponseNotAllowed(["POST"])
async def cached_view(request): etag = compute_etag() if request.headers.get("If-None-Match") == etag: return HttpResponseNotModified() response = HttpResponse(render_content()) response["ETag"] = etag return responseView decorators
Section titled “View decorators”require_http_methods / require_GET / require_POST / require_safe
Section titled “require_http_methods / require_GET / require_POST / require_safe”Restrict a view to specific HTTP methods — returns 405 Method Not Allowed otherwise:
from buraq.decorators import require_http_methods, require_GET, require_POST, require_safe
@require_GETasync def read_only_view(request): ...
@require_POSTasync def submit_view(request): ...
@require_safe # GET and HEADasync def idempotent_view(request): ...
@require_http_methods("GET", "POST")async def multi_method_view(request): ...cache_control
Section titled “cache_control”Set Cache-Control response headers declaratively:
from buraq.decorators import cache_control
@cache_control(max_age=3600, public=True)async def article_detail(request, pk: int): ...
@cache_control(no_cache=True, no_store=True, must_revalidate=True)async def private_view(request): ...never_cache
Section titled “never_cache”Prevent any caching of the response:
from buraq.decorators import never_cache
@never_cacheasync def sensitive_view(request): ...Sets Cache-Control: max-age=0, no-cache, no-store, must-revalidate, private plus Expires and Pragma headers.
vary_on_headers / vary_on_cookie
Section titled “vary_on_headers / vary_on_cookie”Tell caches that the response varies by specific request headers:
from buraq.decorators import vary_on_headers, vary_on_cookie
@vary_on_headers("Accept-Language", "Accept-Encoding")async def localised_view(request): ...
@vary_on_cookieasync def personalised_view(request): ...condition
Section titled “condition”Return 304 Not Modified when the client already has the current version, using ETag and/or Last-Modified callbacks you supply:
from buraq.decorators import condition
def post_etag(request, pk): post = ... # synchronous lookup or cached value return f'"{post.updated_at.isoformat()}"'
def post_last_modified(request, pk): post = ... return post.updated_at # datetime
@condition(etag_func=post_etag, last_modified_func=post_last_modified)async def post_detail(request, pk: int): ...Either argument may be omitted. Async callables are supported.
conditional_page
Section titled “conditional_page”Zero-config ETag support — computes the ETag automatically from the MD5 of the response body:
from buraq.decorators import conditional_page
@conditional_pageasync def article(request, pk: int): ...Use @condition when you can compute the ETag cheaply before hitting the database; use @conditional_page when the body is always rendered anyway.
cache_page
Section titled “cache_page”Cache the full response for a view for N seconds:
from buraq.decorators import cache_page
@cache_page(60 * 15) # cache for 15 minutesasync def article_list(request): ...
# Custom cache backend and key prefix@cache_page(300, cache="secondary", key_prefix="v2")async def search_results(request): ...Only 200 OK responses are cached. Headers that must never be shared (Set-Cookie, Authorization) are stripped before storing.
url_has_allowed_host_and_scheme
Section titled “url_has_allowed_host_and_scheme”Guards against open redirect attacks when redirecting to a user-supplied URL.
from buraq.utils.http import url_has_allowed_host_and_schemefrom buraq.conf.defaults import settingsfrom buraq.shortcuts import redirectfrom buraq.http import HttpResponseBadRequest
async def login_view(request): next_url = request.query_params.get("next", "/dashboard")
if not url_has_allowed_host_and_scheme(next_url, allowed_hosts=set(settings.ALLOWED_HOSTS)): return HttpResponseBadRequest("Unsafe redirect target")
# ... authenticate user ... return redirect(next_url)Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
url |
str | None |
The URL to validate |
allowed_hosts |
str | set[str] |
Allowed hostnames (e.g. {"example.com"}) |
require_https |
bool |
Reject http:// URLs (default False) |
Relative URLs (e.g. /dashboard) are always considered safe. The function rejects:
- Protocol-relative URLs (
//evil.com/steal) - Non-http/https schemes (
javascript:,data:, etc.) - URLs with embedded credentials (
http://user:pass@evil.com) - URLs with newline injection (
/path\r\nX-Header: injected)