From: Michael Tremer Date: Sun, 12 Jan 2025 13:32:05 +0000 (+0000) Subject: web: Make the request handler as async as possible X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8e7cdb40ef3695e29fd50cec7b8884873107a4a4;p=pbs.git web: Make the request handler as async as possible This code is mainly copied from upstream and made to work with async functions for the error handlers. Signed-off-by: Michael Tremer --- diff --git a/src/web/base.py b/src/web/base.py index 034bcd75..f8f23189 100644 --- a/src/web/base.py +++ b/src/web/base.py @@ -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