[IMP] http improvement
- context manager request object (removes some ugly hacks) - improve http error handling - add lazyresponses bzr revid: al@openerp.com-20131203190639-e8r1qm9wc82t8g4k
This commit is contained in:
parent
3e94f7ab1b
commit
43979cfd8f
|
@ -3,6 +3,7 @@
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
import werkzeug.exceptions
|
import werkzeug.exceptions
|
||||||
import werkzeug.routing
|
import werkzeug.routing
|
||||||
|
@ -74,26 +75,29 @@ class ir_http(osv.AbstractModel):
|
||||||
request.disable_db = True
|
request.disable_db = True
|
||||||
request.uid = None
|
request.uid = None
|
||||||
|
|
||||||
def _authenticate(self, func, arguments):
|
def _authenticate(self, auth_method='user'):
|
||||||
auth_method = getattr(func, "auth", "user")
|
|
||||||
if request.session.uid:
|
if request.session.uid:
|
||||||
try:
|
try:
|
||||||
request.session.check_security()
|
request.session.check_security()
|
||||||
# what if error in security.check()
|
# what if error in security.check()
|
||||||
# -> res_users.check()
|
# -> res_users.check()
|
||||||
# -> res_users.check_credentials()
|
# -> res_users.check_credentials()
|
||||||
except Exception:
|
except http.SessionExpiredException:
|
||||||
request.session.logout()
|
request.session.logout()
|
||||||
|
raise http.SessionExpiredException("Session expired for request %s" % request.httprequest)
|
||||||
getattr(self, "_auth_method_%s" % auth_method)()
|
getattr(self, "_auth_method_%s" % auth_method)()
|
||||||
return auth_method
|
return auth_method
|
||||||
|
|
||||||
def _handle_404(self, exception):
|
def _handle_exception(self, exception):
|
||||||
raise exception
|
if isinstance(exception, openerp.exceptions.AccessError):
|
||||||
|
code = 403
|
||||||
|
else:
|
||||||
|
code = getattr(exception, 'code', 500)
|
||||||
|
|
||||||
def _handle_403(self, exception):
|
fn = getattr(self, '_handle_%d' % code, self._handle_unknown_exception)
|
||||||
raise exception
|
return fn(exception)
|
||||||
|
|
||||||
def _handle_500(self, exception):
|
def _handle_unknown_exception(self, exception):
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
def _dispatch(self):
|
def _dispatch(self):
|
||||||
|
@ -101,13 +105,16 @@ class ir_http(osv.AbstractModel):
|
||||||
try:
|
try:
|
||||||
func, arguments = self._find_handler()
|
func, arguments = self._find_handler()
|
||||||
except werkzeug.exceptions.NotFound, e:
|
except werkzeug.exceptions.NotFound, e:
|
||||||
return self._handle_404(e)
|
return self._handle_exception(e)
|
||||||
|
|
||||||
# check authentication level
|
# check authentication level
|
||||||
try:
|
try:
|
||||||
auth_method = self._authenticate(func, arguments)
|
auth_method = self._authenticate(getattr(func, "auth", None))
|
||||||
except werkzeug.exceptions.NotFound, e:
|
except Exception:
|
||||||
return self._handle_403(e)
|
# force a Forbidden exception with the original traceback
|
||||||
|
return self._handle_exception(
|
||||||
|
convert_exception_to(
|
||||||
|
werkzeug.exceptions.Forbidden))
|
||||||
|
|
||||||
# post process arg to set uid on browse records
|
# post process arg to set uid on browse records
|
||||||
for arg in arguments.itervalues():
|
for arg in arguments.itervalues():
|
||||||
|
@ -121,9 +128,7 @@ class ir_http(osv.AbstractModel):
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
raise result
|
raise result
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
fn = getattr(self, '_handle_%s' % getattr(e, 'code', 500),
|
return self._handle_exception(e)
|
||||||
self._handle_500)
|
|
||||||
return fn(e)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -139,4 +144,29 @@ class ir_http(osv.AbstractModel):
|
||||||
|
|
||||||
return self._routing_map
|
return self._routing_map
|
||||||
|
|
||||||
|
def convert_exception_to(to_type, with_message=False):
|
||||||
|
""" Should only be called from an exception handler. Fetches the current
|
||||||
|
exception data from sys.exc_info() and creates a new exception of type
|
||||||
|
``to_type`` with the original traceback.
|
||||||
|
|
||||||
|
If ``with_message`` is ``True``, sets the new exception's message to be
|
||||||
|
the stringification of the original exception. If ``False``, does not
|
||||||
|
set the new exception's message. Otherwise, uses ``with_message`` as the
|
||||||
|
new exception's message.
|
||||||
|
|
||||||
|
:type with_message: str|bool
|
||||||
|
"""
|
||||||
|
etype, original, tb = sys.exc_info()
|
||||||
|
try:
|
||||||
|
if with_message is False:
|
||||||
|
message = None
|
||||||
|
elif with_message is True:
|
||||||
|
message = str(original)
|
||||||
|
else:
|
||||||
|
message = str(with_message)
|
||||||
|
|
||||||
|
raise to_type, message, tb
|
||||||
|
except to_type, e:
|
||||||
|
return e
|
||||||
|
|
||||||
# vim:et:
|
# vim:et:
|
||||||
|
|
130
openerp/http.py
130
openerp/http.py
|
@ -3,7 +3,6 @@
|
||||||
# OpenERP HTTP layer
|
# OpenERP HTTP layer
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
import ast
|
import ast
|
||||||
import cgi
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
|
@ -36,13 +35,20 @@ import werkzeug.wsgi
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
from openerp.service import security, model as service_model
|
from openerp.service import security, model as service_model
|
||||||
from openerp.tools import config
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# RequestHandler
|
# RequestHandler
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
# Thread local global request object
|
||||||
|
_request_stack = werkzeug.local.LocalStack()
|
||||||
|
|
||||||
|
request = _request_stack()
|
||||||
|
"""
|
||||||
|
A global proxy that always redirect to the current request object.
|
||||||
|
"""
|
||||||
|
|
||||||
class WebRequest(object):
|
class WebRequest(object):
|
||||||
""" Parent class for all OpenERP Web request types, mostly deals with
|
""" Parent class for all OpenERP Web request types, mostly deals with
|
||||||
initialization and setup of the request object (the dispatching itself has
|
initialization and setup of the request object (the dispatching itself has
|
||||||
|
@ -138,11 +144,24 @@ class WebRequest(object):
|
||||||
trying to access this property will raise an exception.
|
trying to access this property will raise an exception.
|
||||||
"""
|
"""
|
||||||
# some magic to lazy create the cr
|
# some magic to lazy create the cr
|
||||||
if not self._cr_cm:
|
if not self._cr:
|
||||||
self._cr_cm = self.registry.cursor()
|
self._cr = self.registry.db.cursor()
|
||||||
self._cr = self._cr_cm.__enter__()
|
|
||||||
return self._cr
|
return self._cr
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
_request_stack.push(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
_request_stack.pop()
|
||||||
|
if self._cr:
|
||||||
|
if exc_type is None:
|
||||||
|
self._cr.commit()
|
||||||
|
self._cr.close()
|
||||||
|
# just to be sure no one tries to re-use the request
|
||||||
|
self.disable_db = True
|
||||||
|
self.uid = None
|
||||||
|
|
||||||
def set_handler(self, func, arguments, auth):
|
def set_handler(self, func, arguments, auth):
|
||||||
# is this needed ?
|
# is this needed ?
|
||||||
arguments = dict((k, v) for k, v in arguments.iteritems()
|
arguments = dict((k, v) for k, v in arguments.iteritems()
|
||||||
|
@ -154,39 +173,23 @@ class WebRequest(object):
|
||||||
self.auth_method = auth
|
self.auth_method = auth
|
||||||
|
|
||||||
def _call_function(self, *args, **kwargs):
|
def _call_function(self, *args, **kwargs):
|
||||||
try:
|
request = self
|
||||||
# ugly syntax only to get the __exit__ arguments to pass to self._cr
|
if self.func_request_type != self._request_type:
|
||||||
request = self
|
raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
|
||||||
class with_obj(object):
|
% (self.func, self.httprequest.path, self.func_request_type, self._request_type))
|
||||||
def __enter__(self):
|
|
||||||
pass
|
|
||||||
def __exit__(self, *args):
|
|
||||||
if request._cr_cm:
|
|
||||||
request._cr_cm.__exit__(*args)
|
|
||||||
request._cr_cm = None
|
|
||||||
request._cr = None
|
|
||||||
|
|
||||||
with with_obj():
|
kwargs.update(self.func_arguments)
|
||||||
if self.func_request_type != self._request_type:
|
|
||||||
raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
|
|
||||||
% (self.func, self.httprequest.path, self.func_request_type, self._request_type))
|
|
||||||
|
|
||||||
kwargs.update(self.func_arguments)
|
# Backward for 7.0
|
||||||
|
if getattr(self.func, '_first_arg_is_req', False):
|
||||||
# Backward for 7.0
|
args = (request,) + args
|
||||||
if getattr(self.func, '_first_arg_is_req', False):
|
# Correct exception handling and concurency retry
|
||||||
args = (request,) + args
|
@service_model.check
|
||||||
# Correct exception handling and concurency retry
|
def checked_call(dbname, *a, **kw):
|
||||||
@service_model.check
|
return self.func(*a, **kw)
|
||||||
def checked_call(dbname, *a, **kw):
|
if self.db:
|
||||||
return self.func(*a, **kw)
|
return checked_call(self.db, *args, **kwargs)
|
||||||
if self.db:
|
return self.func(*args, **kwargs)
|
||||||
return checked_call(self.db, *args, **kwargs)
|
|
||||||
return self.func(*args, **kwargs)
|
|
||||||
finally:
|
|
||||||
# just to be sure no one tries to re-use the request
|
|
||||||
self.disable_db = True
|
|
||||||
self.uid = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def debug(self):
|
def debug(self):
|
||||||
|
@ -197,7 +200,7 @@ class WebRequest(object):
|
||||||
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
|
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
|
||||||
yield (self.registry, self.cr)
|
yield (self.registry, self.cr)
|
||||||
|
|
||||||
def route(route, type="http", auth="user"):
|
def route(route, type="http", auth="user", methods=None):
|
||||||
"""
|
"""
|
||||||
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
|
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
|
||||||
of ``Controller``.
|
of ``Controller``.
|
||||||
|
@ -214,6 +217,7 @@ def route(route, type="http", auth="user"):
|
||||||
* ``none``: The method is always active, even if there is no database. Mainly used by the framework and
|
* ``none``: The method is always active, even if there is no database. Mainly used by the framework and
|
||||||
authentication modules. There request code will not have any facilities to access the database nor have any
|
authentication modules. There request code will not have any facilities to access the database nor have any
|
||||||
configuration indicating the current database nor the current user.
|
configuration indicating the current database nor the current user.
|
||||||
|
:param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
|
||||||
"""
|
"""
|
||||||
assert type in ["http", "json"]
|
assert type in ["http", "json"]
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
@ -221,6 +225,7 @@ def route(route, type="http", auth="user"):
|
||||||
f.routes = route
|
f.routes = route
|
||||||
else:
|
else:
|
||||||
f.routes = [route]
|
f.routes = [route]
|
||||||
|
f.methods = methods
|
||||||
f.exposed = type
|
f.exposed = type
|
||||||
if getattr(f, "auth", None) is None:
|
if getattr(f, "auth", None) is None:
|
||||||
f.auth = auth
|
f.auth = auth
|
||||||
|
@ -398,17 +403,8 @@ class HttpRequest(WebRequest):
|
||||||
def dispatch(self):
|
def dispatch(self):
|
||||||
try:
|
try:
|
||||||
r = self._call_function(**self.params)
|
r = self._call_function(**self.params)
|
||||||
except werkzeug.exceptions.HTTPException, e:
|
except (werkzeug.exceptions.HTTPException), e:
|
||||||
r = e
|
r = e
|
||||||
except Exception, e:
|
|
||||||
_logger.exception("An exception occured during an http request")
|
|
||||||
se = serialize_exception(e)
|
|
||||||
error = {
|
|
||||||
'code': 200,
|
|
||||||
'message': "OpenERP Server Error",
|
|
||||||
'data': se
|
|
||||||
}
|
|
||||||
r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
|
|
||||||
else:
|
else:
|
||||||
if not r:
|
if not r:
|
||||||
r = werkzeug.wrappers.Response(status=204) # no content
|
r = werkzeug.wrappers.Response(status=204) # no content
|
||||||
|
@ -451,24 +447,6 @@ def httprequest(f):
|
||||||
base = ""
|
base = ""
|
||||||
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
|
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
|
||||||
# Thread local global request object
|
|
||||||
#----------------------------------------------------------
|
|
||||||
_request_stack = werkzeug.local.LocalStack()
|
|
||||||
|
|
||||||
request = _request_stack()
|
|
||||||
"""
|
|
||||||
A global proxy that always redirect to the current request object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def set_request(req):
|
|
||||||
_request_stack.push(req)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
_request_stack.pop()
|
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Controller and route registration
|
# Controller and route registration
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -526,7 +504,7 @@ def routing_map(modules, nodb_only, converters=None):
|
||||||
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
|
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
|
||||||
if url.endswith("/") and len(url) > 1:
|
if url.endswith("/") and len(url) > 1:
|
||||||
url = url[: -1]
|
url = url[: -1]
|
||||||
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv))
|
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods))
|
||||||
return routing_map
|
return routing_map
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -816,6 +794,18 @@ mimetypes.add_type('application/font-woff', '.woff')
|
||||||
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
|
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
|
||||||
mimetypes.add_type('application/x-font-ttf', '.ttf')
|
mimetypes.add_type('application/x-font-ttf', '.ttf')
|
||||||
|
|
||||||
|
class LazyResponse(werkzeug.wrappers.Response):
|
||||||
|
""" Lazy werkzeug response.
|
||||||
|
API not yet frozen"""
|
||||||
|
|
||||||
|
def __init__(self, callback, **kwargs):
|
||||||
|
super(LazyResponse, self).__init__(mimetype='text/html')
|
||||||
|
self.callback = callback
|
||||||
|
self.params = kwargs
|
||||||
|
def process(self):
|
||||||
|
response = self.callback(**self.params)
|
||||||
|
self.response.append(response)
|
||||||
|
|
||||||
class DisableCacheMiddleware(object):
|
class DisableCacheMiddleware(object):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
@ -941,6 +931,12 @@ class Root(object):
|
||||||
return HttpRequest(httprequest)
|
return HttpRequest(httprequest)
|
||||||
|
|
||||||
def get_response(self, httprequest, result, explicit_session):
|
def get_response(self, httprequest, result, explicit_session):
|
||||||
|
if isinstance(result, LazyResponse):
|
||||||
|
try:
|
||||||
|
result.process()
|
||||||
|
except(Exception), e:
|
||||||
|
result = request.registry['ir.http']._handle_exception(e)
|
||||||
|
|
||||||
if isinstance(result, basestring):
|
if isinstance(result, basestring):
|
||||||
response = werkzeug.wrappers.Response(result, mimetype='text/html')
|
response = werkzeug.wrappers.Response(result, mimetype='text/html')
|
||||||
else:
|
else:
|
||||||
|
@ -980,7 +976,7 @@ class Root(object):
|
||||||
result = request.dispatch()
|
result = request.dispatch()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
with set_request(request):
|
with request:
|
||||||
db = request.session.db
|
db = request.session.db
|
||||||
if db:
|
if db:
|
||||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||||
|
|
Loading…
Reference in New Issue