Skip to content

Commit

Permalink
Return 413 status on over-quota in the native API.
Browse files Browse the repository at this point in the history
Related to LP 1021373.

Previously, we returned a generic 500 Server Error on an over-quota
conditions, whereas this is arguably more appropriately reported
as a 413 status.

Change-Id: I5c1cdc9db54804c512d60e4179c1faa13516d6f9
  • Loading branch information
Eoghan Glynn committed Jul 18, 2012
1 parent c2290f0 commit 6e873bc
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 4 deletions.
21 changes: 17 additions & 4 deletions nova/api/openstack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from nova.api.openstack import wsgi
from nova import exception
from nova import log as logging
from nova import utils
from nova import wsgi as base_wsgi


Expand All @@ -36,11 +37,23 @@
class FaultWrapper(base_wsgi.Middleware):
"""Calls down the middleware stack, making exceptions into faults."""

def _error(self, inner, req, safe=False):
_status_to_type = {}

@staticmethod
def status_to_type(status):
if not FaultWrapper._status_to_type:
for clazz in utils.walk_class_hierarchy(webob.exc.HTTPError):
FaultWrapper._status_to_type[clazz.code] = clazz
return FaultWrapper._status_to_type.get(
status, webob.exc.HTTPInternalServerError)()

def _error(self, inner, req, headers=None, status=500, safe=False):
LOG.exception(_("Caught error: %s"), unicode(inner))
msg_dict = dict(url=req.url, status=500)
msg_dict = dict(url=req.url, status=status)
LOG.info(_("%(url)s returned with HTTP %(status)d") % msg_dict)
outer = webob.exc.HTTPInternalServerError()
outer = self.status_to_type(status)
if headers:
outer.headers = headers
# NOTE(johannes): We leave the explanation empty here on
# purpose. It could possibly have sensitive information
# that should not be returned back to the user. See
Expand All @@ -58,7 +71,7 @@ def __call__(self, req):
try:
return req.get_response(self.application)
except exception.NovaException as ex:
return self._error(ex, req, ex.safe)
return self._error(ex, req, ex.headers, ex.code, ex.safe)
except Exception as ex:
return self._error(ex, req)

Expand Down
4 changes: 4 additions & 0 deletions nova/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ class NovaException(Exception):
"""
message = _("An unknown exception occurred.")
code = 500
headers = {}
safe = False

def __init__(self, message=None, **kwargs):
Expand Down Expand Up @@ -953,6 +955,8 @@ class WillNotSchedule(NovaException):

class QuotaError(NovaException):
message = _("Quota exceeded") + ": code=%(code)s"
code = 413
headers = {'Retry-After': 0}
safe = True


Expand Down
16 changes: 16 additions & 0 deletions nova/tests/api/openstack/compute/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ def test_safe_exceptions_are_described_in_faults(self):
def test_unsafe_exceptions_are_not_described_in_faults(self):
self._do_test_exception_safety_reflected_in_faults(False)

def _do_test_exception_mapping(self, exception_type):
@webob.dec.wsgify
def fail(req):
raise exception_type('too many used')

api = self._wsgi_app(fail)
resp = webob.Request.blank('/').get_response(api)
self.assertTrue('too many used' in resp.body, resp.body)
self.assertEqual(resp.status_int, exception_type.code, resp.body)
for (key, value) in exception_type.headers.iteritems():
self.assertTrue(key in resp.headers)
self.assertEquals(resp.headers[key], value)

def test_quota_error_mapping(self):
self._do_test_exception_mapping(exception.QuotaError)

def test_request_id_in_response(self):
req = webob.Request.blank('/')
req.method = 'GET'
Expand Down
13 changes: 13 additions & 0 deletions nova/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,19 @@ def strcmp_const_time(s1, s2):
return result == 0


def walk_class_hierarchy(clazz, encountered=None):
"""Walk class hierarchy, yielding most derived classes first"""
if not encountered:
encountered = []
for subclass in clazz.__subclasses__():
if subclass not in encountered:
encountered.append(subclass)
# drill down to leaves first
for subsubclass in walk_class_hierarchy(subclass, encountered):
yield subsubclass
yield subclass


class UndoManager(object):
"""Provides a mechanism to facilitate rolling back a series of actions
when an exception is raised.
Expand Down

0 comments on commit 6e873bc

Please sign in to comment.