Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Execute code in Django after response has been sent to the client

In my Django application I want to keep track of whether a response has been sent to the client successfully. I am well aware that there is no "watertight" way in a connectionless protocol like HTTP to ensure the client has received (and displayed) a response, so this will not be mission-critical functionality, but still I want to do this at the latest possible time. The response will not be HTML so any callbacks from the client (using Javascript or IMG tags etc.) are not possible.

The "latest" hook I can find would be adding a custom middleware implementing process_response at the first position of the middleware list, but to my understanding this is executed before the actual response is constructed and sent to the client. Are there any hooks/events in Django to execute code after the response has been sent successfully?

like image 460
Florian Ledermann Avatar asked Nov 30 '10 12:11

Florian Ledermann


4 Answers

The method I am going for at the moment uses a subclass of HttpResponse:

from django.template import loader
from django.http import HttpResponse

# use custom response class to override HttpResponse.close()
class LogSuccessResponse(HttpResponse):

    def close(self):
        super(LogSuccessResponse, self).close()
        # do whatever you want, this is the last codepoint in request handling
        if self.status_code == 200:
            print('HttpResponse successful: %s' % self.status_code)

# this would be the view definition
def logging_view(request):
    response = LogSuccessResponse('Hello World', mimetype='text/plain')
    return response

By reading the Django code I am very much convinced that HttpResponse.close() is the latest point to inject code into the request handling. I am not sure if there really are error cases that are handled better by this method compared to the ones mentioned above, so I am leaving the question open for now.

The reasons I prefer this approach to the others mentioned in lazerscience's answer are that it can be set up in the view alone and does not require middleware to be installed. Using the request_finished signal, on the other hand, wouldn't allow me to access the response object.

like image 116
Florian Ledermann Avatar answered Nov 19 '22 15:11

Florian Ledermann


If you need to do this a lot, a useful trick is to have a special response class like:

class ResponseThen(Response):
    def __init__(self, data, then_callback, **kwargs):
        super().__init__(data, **kwargs)
        self.then_callback = then_callback

    def close(self):
        super().close()
        self.then_callback()

def some_view(request):
    # ...code to run before response is returned to client

    def do_after():
        # ...code to run *after* response is returned to client

    return ResponseThen(some_data, do_after, status=status.HTTP_200_OK)

...helps if you want a quick/hacky "fire and forget" solution without bothering to integrate a proper task queue or split off a separate microservice from your app.

like image 28
NeuronQ Avatar answered Nov 19 '22 17:11

NeuronQ


I suppose when talking about middleware you are thinking about the middleware's process_request method, but there's also a process_response method that is called when the HttpResponse object is returned. I guess that will be the latest moment where you can find a hook that you can use.

Furthermore there's also a request_finished signal being fired.

like image 3
Bernhard Vallant Avatar answered Nov 19 '22 16:11

Bernhard Vallant


I found a filthy trick to do this by accessing a protected member in HttpResponse.

def some_view(request):
    # ...code to run before response is returned to client

    def do_after():
        # ...code to run *after* response is returned to client

    response = HttpResponse()
    response._resource_closers.append(do_after)
    return response

It works in Django 3.0.6 , check the "close" function in the prototype of HttpResponse.

def close(self):
    for closer in self._resource_closers:
        try:
            closer()
        except Exception:
            pass
    # Free resources that were still referenced.
    self._resource_closers.clear()
    self.closed = True
    signals.request_finished.send(sender=self._handler_class)
like image 1
F814 Del Avatar answered Nov 19 '22 17:11

F814 Del