Standardmäßig weist Django jeden Webhook-POST mit einem 403 ab. Schuld ist die CSRF-Middleware, die ihren Job macht — Schutz vor Cross-Site Request Forgery —, nur ist der Anbieter kein Browser und trägt weder Session-Cookie noch CSRF-Token. Der Fix sind ein Decorator und eine Zeile Bewusstsein über den Zugriff auf den Request-Body.
CSRF für Webhook-Views überspringen
Der Django-Weg:
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, HttpResponseBadRequest
import hmac, hashlib
@csrf_exempt
def stripe_webhook(request):
if request.method != 'POST':
return HttpResponseBadRequest('POST only')
payload = request.body # rohe Bytes, unangetastet
signature = request.headers.get('Stripe-Signature', '')
# Signatur prüfen (im echten Code das Stripe-SDK nutzen)
if not verify(payload, signature, settings.STRIPE_WEBHOOK_SECRET):
return HttpResponse(status=401)
event = json.loads(payload)
handle_stripe_event(event)
return HttpResponse(status=200)
Der Decorator @csrf_exempt entfernt die Middleware-Prüfung nur für diese View — deine anderen Formulare behalten den Schutz. request.body liefert die rohen Bytes, wie sie ankamen, genau das, was die Signaturprüfung braucht.
Wenn du Django REST Framework nutzt
DRF verpackt Requests in ein eigenes Request-Objekt und startet Parser je nach Content-Type. Wenn dein View-Code request.data liest, hat der JSON-Parser den Body bereits verbraucht. Zwei Auswege:
Option A: als einfache Django-View belassen
Webhook-Handler brauchen DRFs Parser, Browsable API oder Auth-Maschinerie nicht. Eine einfache @csrf_exempt-View neben deinen DRF-Endpoints ist das sauberste Muster.
Option B: DRF mit Raw-Zugriff
Willst du wirklich DRF, lies request.body (den darunterliegenden Django-Request) statt request.data:
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny
@api_view(['POST'])
@authentication_classes([]) # keine Auth für Webhooks
@permission_classes([AllowAny]) # Anbieter authentifiziert bereits per Signatur
def stripe_webhook(request):
payload = request._request.body # darunterliegender Django-Request, rohe Bytes
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
# ...prüfen und behandeln
Die Indirektion request._request ist umständlich, aber der offizielle Ausweg, wenn du DRFs Parsing umgehen musst.
Tunnel einrichten
- Starte deinen Django-Dev-Server:
python manage.py runserver 8000. - Füge den Hostnamen des Tunnels in
settings.pyzuALLOWED_HOSTShinzu. Am einfachsten ist['*']im lokalen Dev, oder eine spezifische Tunnel-Domain, wenn du das bevorzugst. npx portpreview 8000in einem separaten Terminal.- Füge die Tunnel-URL plus deinen Webhook-Pfad ins Dashboard des Anbieters ein.
- Löse ein Test-Event aus und sieh zu, wie es in deiner View landet.
ALLOWED_HOSTS zu vergessen erzeugt einen generischen Django-400 ohne offensichtliche Ursache. Gibt deine erste Webhook-Zustellung 400 zurück und du siehst keine View-Logs, ist das der Grund.
Häufige Stolperfallen
Body zweimal lesen
Wie jedes Framework gibt dir Django den Body als Stream, der nur einmal gelesen werden kann. Hat Middleware oder ein Logging-Wrapper request.body bereits angefasst, sieht dein View-Code ein leeres Bytes-Objekt. Gib zur Kontrolle len(request.body) aus.
Zeitzonen und Timestamp-Toleranz
Vergleichst du den signierten Timestamp mit datetime.now(), nutze timezone.now() oder vergleiche in UTC. Naive Datetime-Vergleiche scheitern sporadisch bei Sommerzeitumstellungen.
Async-Views
Django 4+ unterstützt async def-Views. Webhook-Handler können async sein, aber beachte, dass Signaturprüfungs-SDKs (Stripe Python usw.) synchron sind. Führe sie mit asgiref.sync_to_async aus oder halte die ganze View synchron.
Hinweise zum Produktions-Deployment
Bist du am lokalen Testen vorbei, läuft dieselbe View in Produktion. Gunicorn, uWSGI, Daphne — keiner fasst request.body an, bevor deine View ihn sieht. Dein Reverse-Proxy vielleicht (z. B. nginx mit eingeschaltetem proxy_buffering), aber das betrifft Pufferung, nicht Body-Veränderung.
Zur zugrunde liegenden Signaturtheorie siehe den Leitfaden zur Signaturprüfung. Für Stripe-spezifische Muster behandelt der Stripe-Local-Testing-Guide die Dashboard-Seite. Tritt der PortPreview-Warteliste bei für Tunnel, die mit Python-Dev-Servern gut harmonieren.