# HG changeset patch # User Goffi # Date 1592578070 -7200 # Node ID cad8f24e23d4ad5fbb082bcf895fb967b73fa0ba # Parent 334d044f2713e16294d83f43828f164d73170c31 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. diff -r 334d044f2713 -r cad8f24e23d4 libervia/server/pages.py --- 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