diff libervia/server/pages.py @ 1276:cad8f24e23d4

pages: use a coroutine for renderPage: renderPage is now a coroutine, and pages methods are launched using asDeferred, so they can be coroutines too. Full traceback is now logged for uncatched errors.
author Goffi <goffi@goffi.org>
date Fri, 19 Jun 2020 16:47:50 +0200
parents 334d044f2713
children 2e4fcd31f2a9
line wrap: on
line diff
--- a/libervia/server/pages.py	Fri Jun 19 16:47:50 2020 +0200
+++ b/libervia/server/pages.py	Fri Jun 19 16:47:50 2020 +0200
@@ -25,6 +25,7 @@
 import hashlib
 import copy
 import json
+import traceback
 from pathlib import Path
 from functools import reduce
 from typing import Optional, List
@@ -38,6 +39,7 @@
 
 from sat.core.i18n import _
 from sat.core import exceptions
+from sat.tools.utils import asDeferred
 from sat.tools.common import date_utils
 from sat.tools.common import utils
 from sat.core.log import getLogger
@@ -228,8 +230,6 @@
             return
         if template is not None and render is not None:
             log.error(_("render and template methods can't be used at the same time"))
-        if parse_url is not None and not callable(parse_url):
-            log.error(_("parse_url must be a callable"))
 
         # if not None, next rendering will be cached
         #  it must then contain a list of the the keys to use (without the page instance)
@@ -1043,7 +1043,7 @@
         request.finish()
         raise failure.Failure(exceptions.CancelError("cache is used"))
 
-    def _cacheURL(self, __, request, profile):
+    def _cacheURL(self, request, profile):
         self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request)
 
     @classmethod
@@ -1220,7 +1220,9 @@
             redirect_page._do_cache = self._do_cache
             self._do_cache = None
 
-        redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
+        defer.ensureDeferred(
+            redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
+        )
         raise failure.Failure(exceptions.CancelError("page redirection is used"))
 
     def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False):
@@ -1280,7 +1282,7 @@
             return
         request.finish()
 
-    def _subpagesHandler(self, __, request):
+    def _subpagesHandler(self, request):
         """render subpage if suitable
 
         this method checks if there is still an unmanaged part of the path
@@ -1298,7 +1300,7 @@
                 child.render(request)
                 raise failure.Failure(exceptions.CancelError("subpage page is used"))
 
-    def _prepare_dynamic(self, __, request):
+    def _prepare_dynamic(self, request):
         # we need to activate dynamic page
         # we set data for template, and create/register token
         socket_token = str(uuid.uuid4())
@@ -1313,13 +1315,7 @@
         request._signals_cache = []
         self.host.registerWSToken(socket_token, self, request)
 
-    def _prepare_render(self, __, request):
-        return defer.maybeDeferred(self.prepare_render, self, request)
-
-    def _render_method(self, __, request):
-        return defer.maybeDeferred(self.render_method, self, request)
-
-    def _render_template(self, __, request):
+    def _render_template(self, request):
         template_data = request.template_data
 
         # if confirm variable is set in case of successfuly data post
@@ -1365,30 +1361,6 @@
             main_menu=self.main_menu,
             **template_data)
 
-    def _renderEb(self, failure_, request):
-        """don't raise error on CancelError"""
-        failure_.trap(exceptions.CancelError)
-
-    def _internalError(self, failure_, request):
-        """called if an error is not catched"""
-        if failure_.check(BridgeException) and failure_.value.condition == 'not-allowed':
-            log.warning("not allowed exception catched")
-            self.pageError(request, C.HTTP_FORBIDDEN)
-        log.error(_("Uncatched error for HTTP request on {url}: {msg}")
-            .format(url=request.URLPath(), msg=failure_))
-        self.pageError(request, C.HTTP_INTERNAL_ERROR)
-
-    def _on_data_post_error(self, failure_, request):
-        failure_.trap(exceptions.DataError)
-        # something is wrong with the posted data, we re-display the page with a
-        # warning notification
-        session_data = self.host.getSessionData(request, session_iface.ISATSession)
-        session_data.setPageNotification(self, failure_.value.message, C.LVL_WARNING)
-        request.setResponseCode(C.HTTP_SEE_OTHER)
-        request.setHeader("location", request.uri)
-        request.finish()
-        raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
-
     def _on_data_post_redirect(self, ret, request):
         """called when page's on_data_post has been done successfuly
 
@@ -1434,7 +1406,7 @@
         request.finish()
         raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
 
-    def _on_data_post(self, __, request):
+    async def _on_data_post(self, request):
         csrf_token = self.host.getSessionData(
             request, session_iface.ISATSession
         ).csrf_token
@@ -1449,10 +1421,19 @@
                 )
             )
             self.pageError(request, C.HTTP_FORBIDDEN)
-        d = defer.maybeDeferred(self.on_data_post, self, request)
-        d.addCallback(self._on_data_post_redirect, request)
-        d.addErrback(self._on_data_post_error, request)
-        return d
+        try:
+            ret = await asDeferred(self.on_data_post, self, request)
+        except exceptions.DataError as e:
+            # something is wrong with the posted data, we re-display the page with a
+            # warning notification
+            session_data = self.host.getSessionData(request, session_iface.ISATSession)
+            session_data.setPageNotification(self, e.value.message, C.LVL_WARNING)
+            request.setResponseCode(C.HTTP_SEE_OTHER)
+            request.setHeader("location", request.uri)
+            request.finish()
+            raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
+        else:
+            self._on_data_post_redirect(ret, request)
 
     def getPostedData(self, request, keys, multiple=False, raise_on_missing=True):
         """Get data from a POST request or from URL's query part and decode it
@@ -1542,7 +1523,7 @@
             request.data = {}
             return request.data
 
-    def _checkAccess(self, data, request):
+    def _checkAccess(self, request):
         """Check access according to self.access
 
         if access is not granted, show a HTTP_FORBIDDEN pageError and stop request,
@@ -1562,8 +1543,6 @@
                     login_url = self.getPageRedirectURL(request)
                     self.HTTPRedirect(request, login_url)
 
-        return data
-
     def setBestLocale(self, request):
         """Guess the best locale when it is not specified explicitly by user
 
@@ -1642,7 +1621,7 @@
             log.error("Can't renderAndUpdate, html was: {html}".format(html=html))
             raise e
 
-    def renderPage(self, request, skip_parse_url=False):
+    async def renderPage(self, request, skip_parse_url=False):
         """Main method to handle the workflow of a LiberviaPage"""
 
         # template_data are the variables passed to template
@@ -1695,67 +1674,101 @@
                         session_data.theme = theme
 
 
-        d = defer.Deferred()
-        d.addCallback(self._checkAccess, request)
+        try:
+
+            try:
+                self._checkAccess(request)
+
+                if self.redirect is not None:
+                    self.pageRedirect(self.redirect, request, skip_parse_url=False)
+
+                if self.parse_url is not None and not skip_parse_url:
+                    if self.url_cache:
+                        profile = self.getProfile(request)
+                        try:
+                            cache_url = self.cached_urls[profile][request.uri]
+                        except KeyError:
+                            # no cache for this URI yet
+                            #  we do normal URL parsing, and then the cache
+                            await asDeferred(self.parse_url, self, request)
+                            self._cacheURL(request, profile)
+                        else:
+                            log.debug(f"using URI cache for {self}")
+                            cache_url.use(request)
+                    else:
+                        await asDeferred(self.parse_url, self, request)
+
+                self._subpagesHandler(request)
+
+                if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
+                    # only HTTP GET and POST are handled so far
+                    self.pageError(request, C.HTTP_BAD_REQUEST)
+
+                if request.method == C.HTTP_METHOD_POST:
+                    if self.on_data_post is None:
+                        # if we don't have on_data_post, the page was not expecting POST
+                        # so we return an error
+                        self.pageError(request, C.HTTP_BAD_REQUEST)
+                    else:
+                        await self._on_data_post(request)
+                    # by default, POST follow normal behaviour after on_data_post is called
+                    # this can be changed by a redirection or other method call in on_data_post
 
-        if self.redirect is not None:
-            d.addCallback(
-                lambda __: self.pageRedirect(
-                    self.redirect, request, skip_parse_url=False
+                if self.dynamic:
+                    self._prepare_dynamic(request)
+
+                if self.prepare_render:
+                    await asDeferred(self.prepare_render, self, request)
+
+                if self.template:
+                    rendered = self._render_template(request)
+                elif self.render_method:
+                    rendered = await asDeferred(self.render_method, self, request)
+                else:
+                    raise exceptions.InternalError(
+                        "No method set to render page, please set a template or use a "
+                        "render method"
+                    )
+
+                self.writeData(rendered, request)
+
+            except failure.Failure as f:
+                # we have to unpack to Failure to catch the right Exception
+                raise f.value
+
+        except exceptions.CancelError:
+            pass
+        except BridgeException as e:
+            if e.condition == 'not-allowed':
+                log.warning("not allowed exception catched")
+                self.pageError(request, C.HTTP_FORBIDDEN)
+            else:
+                log.error(_("Uncatched bridge exception for HTTP request on {url}: {e}")
+                    .format(url=request.URLPath(), e=e))
+                try:
+                    self.pageError(request, C.HTTP_INTERNAL_ERROR)
+                except exceptions.CancelError:
+                    pass
+        except Exception as e:
+            tb = traceback.format_exc()
+            log.error(
+                _("Uncatched error for HTTP request on {url}:\n{tb}")
+                .format(
+                    url=request.URLPath(),
+                    e_name=e.__class__.__name__,
+                    e=e,
+                    tb=tb,
                 )
             )
-
-        if self.parse_url is not None and not skip_parse_url:
-            if self.url_cache:
-                profile = self.getProfile(request)
-                try:
-                    cache_url = self.cached_urls[profile][request.uri]
-                except KeyError:
-                    # no cache for this URI yet
-                    #  we do normal URL parsing, and then the cache
-                    d.addCallback(self.parse_url, request)
-                    d.addCallback(self._cacheURL, request, profile)
-                else:
-                    log.debug(_("using URI cache for {page}").format(page=self))
-                    cache_url.use(request)
-            else:
-                d.addCallback(self.parse_url, request)
-
-        d.addCallback(self._subpagesHandler, request)
-
-        if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
-            # only HTTP GET and POST are handled so far
-            d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
+            try:
+                self.pageError(request, C.HTTP_INTERNAL_ERROR)
+            except exceptions.CancelError:
+                pass
 
-        if request.method == C.HTTP_METHOD_POST:
-            if self.on_data_post is None:
-                # if we don't have on_data_post, the page was not expecting POST
-                # so we return an error
-                d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
-            else:
-                d.addCallback(self._on_data_post, request)
-            # by default, POST follow normal behaviour after on_data_post is called
-            # this can be changed by a redirection or other method call in on_data_post
-
-        if self.dynamic:
-            d.addCallback(self._prepare_dynamic, request)
-
-        if self.prepare_render:
-            d.addCallback(self._prepare_render, request)
-
-        if self.template:
-            d.addCallback(self._render_template, request)
-        elif self.render_method:
-            d.addCallback(self._render_method, request)
-
-        d.addCallback(self.writeData, request)
-        d.addErrback(self._renderEb, request)
-        d.addErrback(self._internalError, request)
-        d.callback(self)
+    def render_GET(self, request):
+        defer.ensureDeferred(self.renderPage(request))
         return server.NOT_DONE_YET
 
-    def render_GET(self, request):
-        return self.renderPage(request)
-
     def render_POST(self, request):
-        return self.renderPage(request)
+        defer.ensureDeferred(self.renderPage(request))
+        return server.NOT_DONE_YET