Django Request Signals
Introduction
Django's signal framework provides a powerful way to allow decoupled applications to communicate when certain events occur. While we've explored the basic model signals in previous sections, Django also provides specialized signals related to the HTTP request-response cycle. These request signals allow your code to execute actions when requests are received or responses are about to be sent.
Request signals help you implement cross-cutting concerns like logging, monitoring, or analytics without cluttering your view logic. They're particularly useful for functionality that needs to be applied across many views consistently.
Request Signals in Django
Django provides two main request-related signals:
request_started
- Sent when Django begins processing an HTTP requestrequest_finished
- Sent when Django finishes processing an HTTP request
Let's explore how these signals work and how you can use them in your applications.
The Request-Response Lifecycle and Signals
Before diving into the signals, it's helpful to understand where they fit in Django's request-response cycle:
- A client sends an HTTP request
- Django's server receives the request
request_started
signal fires- Request passes through middleware
- URL routing determines the view
- View processes the request and returns a response
- Response passes back through middleware
request_finished
signal fires- Response is sent to the client
As you can see, request signals act as bookends around the entire request handling process.
Setting Up Request Signals
To use request signals, first make sure to import them from the correct module:
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
The request_started
Signal
This signal is sent when Django begins processing a request. Here's how to connect a function to it:
@receiver(request_started)
def my_request_started_handler(sender, **kwargs):
"""
Executed whenever a new request is received by Django
Parameters:
- sender: The handler class that triggered the signal (usually WSGIHandler)
- kwargs: Any additional parameters
"""
print("A request has started processing!")
# You can do initialization, logging, or other setup here
The request_finished
Signal
This signal fires when Django finishes processing a request, right before the response is sent to the client:
@receiver(request_finished)
def my_request_finished_handler(sender, **kwargs):
"""
Executed when Django is about to send the response
Parameters:
- sender: The handler class that triggered the signal (usually WSGIHandler)
- kwargs: Any additional parameters
"""
print("A request has finished processing!")
# Good place for cleanup, metrics recording, etc.
Practical Examples
Example 1: Request Timing Middleware Using Signals
Let's create a solution that measures how long each request takes to process:
# In a file named timing.py in your app
import time
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
# Store the start time in a thread-local variable
_request_start_time = {}
@receiver(request_started)
def start_timer(sender, **kwargs):
# Store the current timestamp
_request_start_time[time.thread_ident] = time.time()
@receiver(request_finished)
def log_request_time(sender, **kwargs):
thread_id = time.thread_ident
if thread_id in _request_start_time:
duration = time.time() - _request_start_time[thread_id]
print(f"Request processed in {duration:.2f} seconds")
# Clean up our thread-local storage
del _request_start_time[thread_id]
Add this to your AppConfig.ready()
method to register the signals:
# In your app's apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
# Import the signals module to register signals
import myapp.timing
Example 2: Request Analytics Tracker
Here's a more practical example that logs analytics data for each request:
# analytics.py
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
import logging
logger = logging.getLogger('request_analytics')
@receiver(request_started)
def track_request_start(sender, environ, **kwargs):
"""
Log the beginning of each request with its path and method
The environ parameter contains the WSGI environment
"""
if environ:
path = environ.get('PATH_INFO', 'unknown')
method = environ.get('REQUEST_METHOD', 'unknown')
logger.info(f"Request started: {method} {path}")
@receiver(request_finished)
def track_request_end(sender, **kwargs):
"""Log the completion of a request"""
logger.info(f"Request completed")
# In a real implementation, you might gather more data here
# like response status, size, etc.
Important Considerations
-
Performance Impact: Signal handlers run on every request, so keep them lightweight to avoid performance issues.
-
No Request Object: Unlike middleware, the
request_started
signal doesn't receive the fullHttpRequest
object, only the WSGI environment. If you need the request object, consider using middleware instead. -
Thread Safety: If running Django with multiple threads, make sure your signal handlers are thread-safe, as shown in the timing example.
-
Order of Execution: You can't control the exact order in which signal handlers run if multiple handlers are connected to the same signal.
When to Use Request Signals vs. Middleware
While request signals and middleware can solve similar problems, each has its strengths:
Use Request Signals when:
- You need extremely simple pre/post request processing
- The functionality doesn't need the full request/response objects
- You want to decouple the logic from your apps
Use Middleware when:
- You need to modify the request or response objects
- You need fine-grained control over the processing order
- You need access to the view being processed
Creating Your Own Request-Related Signals
Beyond the built-in request signals, you can create custom signals for specific points in your request processing:
# In your app's signals.py
from django.dispatch import Signal
# Define custom signals
api_request_started = Signal(providing_args=["request"])
api_request_finished = Signal(providing_args=["request", "response"])
# In your views.py
from myapp.signals import api_request_started, api_request_finished
def api_view(request):
# Send our custom signal
api_request_started.send(sender=api_view, request=request)
# Process the request...
response = {...}
# Send our finish signal
api_request_finished.send(
sender=api_view,
request=request,
response=response
)
return response
Summary
Django's request signals provide a way to hook into the beginning and end of the request-response cycle. They're useful for implementing cross-cutting concerns like logging, performance monitoring, and analytics. While simpler than middleware, they provide less flexibility since they don't have full access to the request and response objects.
Key points to remember about request signals:
- They fire at the very beginning and end of request processing
- They're decoupled from your view logic
- They're simpler but less powerful than middleware
- They should be kept lightweight to avoid performance issues
Additional Resources
Exercises
- Implement a request signal that logs all POST requests to a specific file.
- Create a signal handler that counts the number of requests processed by your application and prints a report every 100 requests.
- Combine request signals with Django's caching framework to implement a simple request rate limiter.
- Build a custom signal that fires whenever a specific view is accessed, and use it to track user behavior.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)