[REF] core: HTTPocalypse (1) rationnals

This commit is the 1st commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

Our application framework is quite complicated. There are a few explicit
middlewares: `ProxyFix`, `DisableCacheMiddleware`, `SharedDataMiddleware`.
There are many (13, in total) implicit ones in the form of
`ir.http._dispatch` overrides. There are a few frames that don't have
much values by themselves and are very much implementation details
there are: `application_unproxied`, `_call_function`, `checked_call` and
`EndPoint.__call__`.

The ORM initialisation is quite magical, the cursor, registry and
environment are all setup lazily thanks to class properties. The
context and uid of the environment are aliased on the request. The
abuse of properties makes it impossible to reason about where and when
the environment is actually instantiated and modified.

The dispatch of a request follow the next scheme:
`odoo.http/Root.dispatch` -> `odoo.addons.base.models/ir.http._dispatch`
-> `odoo.http/(Http|Json)Request.dispatch` -> `odoo.http/@route` ->
controller. Because the request becomes http or json specialized quite
late, the many ir.http override all need to parsimony try/except their
code in order to manually call `_handle_exception()` uppon error, they
cannot let the error bubble-up. This back-and-forth between odoo.http
and ir.http is source of some headaches.

Speaking about error, the `odoo.http/WebRequest._handle_exception`
implementation is quite complicated, it basically craft a new exception
out of the passed exception object in order to correct its traceback.
If the error had been bubbled-up instead of handled by
`_handle_exception()` such python hack would not have been necessary.

The objectives of this refactor are about refounding the technical dept:

* We want shorter error reporting;
* We want simpler request and ir.http APIs;
* We want better integration of all the ir.http extensions.

PR: odoo#78857
Task: 2571224
This commit is contained in:
Julien Castiaux
2022-02-24 13:30:47 +00:00
parent 169dd13df3
commit c3714eafbd
2 changed files with 0 additions and 1822 deletions
-1710
View File
File diff suppressed because it is too large Load Diff
-112
View File
@@ -1,112 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
WSGI stack, common code.
"""
import logging
import sys
import threading
import traceback
from xmlrpc import client as xmlrpclib
import werkzeug.exceptions
import werkzeug.wrappers
import werkzeug.serving
import odoo
from odoo.tools import config
_logger = logging.getLogger(__name__)
# XML-RPC fault codes. Some care must be taken when changing these: the
# constants are also defined client-side and must remain in sync.
# User code must use the exceptions defined in ``odoo.exceptions`` (not
# create directly ``xmlrpclib.Fault`` objects).
RPC_FAULT_CODE_CLIENT_ERROR = 1 # indistinguishable from app. error.
RPC_FAULT_CODE_APPLICATION_ERROR = 1
RPC_FAULT_CODE_WARNING = 2
RPC_FAULT_CODE_ACCESS_DENIED = 3
RPC_FAULT_CODE_ACCESS_ERROR = 4
def xmlrpc_handle_exception_int(e):
if isinstance(e, odoo.exceptions.RedirectWarning):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
elif isinstance(e, odoo.exceptions.AccessError):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
elif isinstance(e, odoo.exceptions.AccessDenied):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
elif isinstance(e, odoo.exceptions.UserError):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
else:
info = sys.exc_info()
# Which one is the best ?
formatted_info = "".join(traceback.format_exception(*info))
#formatted_info = odoo.tools.exception_to_unicode(e) + '\n' + info
fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
return xmlrpclib.dumps(fault, allow_none=None)
def xmlrpc_handle_exception_string(e):
if isinstance(e, odoo.exceptions.RedirectWarning):
fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
elif isinstance(e, odoo.exceptions.MissingError):
fault = xmlrpclib.Fault('warning -- MissingError\n\n' + str(e), '')
elif isinstance(e, odoo.exceptions.AccessError):
fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
elif isinstance(e, odoo.exceptions.AccessDenied):
fault = xmlrpclib.Fault('AccessDenied', str(e))
elif isinstance(e, odoo.exceptions.UserError):
fault = xmlrpclib.Fault('warning -- UserError\n\n' + str(e), '')
#InternalError
else:
info = sys.exc_info()
formatted_info = "".join(traceback.format_exception(*info))
fault = xmlrpclib.Fault(odoo.tools.exception_to_unicode(e), formatted_info)
return xmlrpclib.dumps(fault, allow_none=None, encoding=None)
def application_unproxied(environ, start_response):
""" WSGI entry point."""
# cleanup db/uid trackers - they're set at HTTP dispatch in
# web.session.OpenERPSession.send() and at RPC dispatch in
# odoo.service.web_services.objects_proxy.dispatch().
# /!\ The cleanup cannot be done at the end of this `application`
# method because werkzeug still produces relevant logging afterwards
if hasattr(threading.current_thread(), 'uid'):
del threading.current_thread().uid
if hasattr(threading.current_thread(), 'dbname'):
del threading.current_thread().dbname
if hasattr(threading.current_thread(), 'url'):
del threading.current_thread().url
result = odoo.http.root(environ, start_response)
if result is not None:
return result
# We never returned from the loop.
return werkzeug.exceptions.NotFound("No handler found.\n")(environ, start_response)
try:
# werkzeug >= 0.15
from werkzeug.middleware.proxy_fix import ProxyFix as ProxyFix_
# 0.15 also supports port and prefix, but 0.14 only forwarded for, proto
# and host so replicate that
ProxyFix = lambda app: ProxyFix_(app, x_for=1, x_proto=1, x_host=1)
except ImportError:
# werkzeug < 0.15
from werkzeug.contrib.fixers import ProxyFix
def application(environ, start_response):
# FIXME: is checking for the presence of HTTP_X_FORWARDED_HOST really useful?
# we're ignoring the user configuration, and that means we won't
# support the standardised Forwarded header once werkzeug supports
# it
if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
return ProxyFix(application_unproxied)(environ, start_response)
else:
return application_unproxied(environ, start_response)