]> git.ipfire.org Git - pbs.git/commitdiff
web: Make the request handler as async as possible
authorMichael Tremer <michael.tremer@ipfire.org>
Sun, 12 Jan 2025 13:32:05 +0000 (13:32 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Sun, 12 Jan 2025 13:32:05 +0000 (13:32 +0000)
This code is mainly copied from upstream and made to work with async
functions for the error handlers.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/web/base.py

index 034bcd75ebc36e293eebfeb76cd23428c7ddbaa8..f8f231899dfc485757bf6a4bd3447294d3f89bca 100644 (file)
@@ -10,7 +10,9 @@ import kerberos
 import logging
 import os
 import socket
+import sys
 import time
+import tornado.concurrent
 import tornado.locale
 import tornado.web
 import tornado.websocket
@@ -277,7 +279,133 @@ class BaseHandler(tornado.web.RequestHandler):
                # Send the response
                self.finish(html)
 
-       def write_error(self, code, exc_info=None, **kwargs):
+       async def _execute(self, transforms, *args, **kwargs):
+               """
+                       Executes this request
+               """
+               self._transforms = transforms
+
+               try:
+                       # Raise error if the method is not supported
+                       if self.request.method not in self.SUPPORTED_METHODS:
+                               raise HTTPError(405)
+
+                       # Parse arguments
+                       self.path_args = [self.decode_argument(arg) for arg in args]
+                       self.path_kwargs = {
+                               k : self.decode_argument(v, name=k) for (k, v) in kwargs.items()
+                       }
+
+                       # Check the XSRF cookie
+                       if not self.request.method in ("GET", "HEAD", "OPTIONS"):
+                               self.check_xsrf_cookie()
+
+                       # Prepare the request
+                       result = self.prepare()
+                       if result:
+                               await result
+
+                       # Tell the application we are now ready to receive the body
+                       if self._prepared_future:
+                               tornado.concurrent.future_set_result_unless_cancelled(
+                                       self._prepared_future, None
+                               )
+                       if self._finished:
+                               return
+
+                       # In streaming mode, we have to wait until the entire body has been received
+                       if tornado.web._has_stream_request_body(self.__class__):
+                               try:
+                                       await self.request._body_future
+                               except tornado.iostream.StreamClosedError:
+                                       return
+
+                       # Fetch the implementation
+                       method = getattr(self, self.request.method.lower())
+
+                       # Call the method
+                       result = method(*self.path_args, **self.path_kwargs)
+                       if result:
+                               await result
+
+                       # Automatically finish?
+                       if self._auto_finish and not self._finished:
+                               self.finish()
+
+               except Exception as e:
+                       try:
+                               await self._handle_request_exception(e)
+                       except Exception:
+                               log.error("Exception in exception handler", exc_info=True)
+
+       async def _handle_request_exception(self, e):
+               # Not really an error, just finish the request
+               if isinstance(e, tornado.web.Finish):
+                       if not self._finished:
+                               self.finish(*e.args)
+                       return
+
+               # Fetch more information about this exception
+               exc_info = sys.exc_info()
+
+               # Log the exception
+               try:
+                       self.log_exception(*exc_info)
+               except Exception:
+                       log.error("Error in exception logger", exc_info=True)
+
+               # We cannot send an error if something has already been sent,
+               # so we just log the exception and are done.
+               if self._finished:
+                       return
+
+               if isinstance(e, tornado.web.HTTPError):
+                       await self.send_error(e.status_code, exc_info=exc_info)
+               else:
+                       await self.send_error(500, exc_info=exc_info)
+
+       async def send_error(self, status_code=500, **kwargs):
+               """
+                       Sends a HTTP error to the browser.
+               """
+               if self._headers_written:
+                       log.error("Cannot send error response after headers written")
+                       if not self._finished:
+                               try:
+                                       self.finish()
+                               except Exception:
+                                       log.error("Failed to flush partial response", exc_info=True)
+                       return
+
+               # Clear any headers that have been set by the handler
+               self.clear()
+
+               # Fetch the reason
+               reason = kwargs.get("reason")
+
+               # Try to extract the reason from the exception
+               try:
+                       type, exception, traceback = kwargs["exc_info"]
+               except KeyError:
+                       pass
+               else:
+                       if isinstance(exception, tornado.web.HTTPError) and exception.reason:
+                               reason = exception.reason
+
+               # Set the status code
+               self.set_status(status_code, reason=reason)
+
+               # Render the error message
+               try:
+                       await self.write_error(status_code, **kwargs)
+               except Exception:
+                       log.error("Uncaught exception in write_error", exc_info=True)
+
+               # Make sure we are finished now to release the socket
+               if not self._finished:
+                       self.finish()
+
+       async def write_error(self, code, exc_info=None, **kwargs):
                try:
                        message = http.client.responses[code]
                except KeyError:
@@ -291,7 +419,7 @@ class BaseHandler(tornado.web.RequestHandler):
                                if self.current_user.is_admin():
                                        _traceback += traceback.format_exception(*exc_info)
 
-               self.render("errors/error.html",
+               await self.render("errors/error.html",
                        code=code, message=message, traceback="".join(_traceback), **kwargs)
 
        # Typed Arguments