Changes HTTP response code for unsupported methods
Requests for resources with an unsupported HTTP method now return a HTTP response 405 (Method Not Allowed) or 501 (Not Implemented) rather than a 404 (Not Found) for everything. For example, attempting to DELETE on /v2/images will now return a 405 instead of a 404 and will provide a response header 'Allow' that lists the valid methods for the resource. Attempting to use NON_EXISTENT_METHOD on /v2/images will now return a 501. Attempting to GET on /v2/non_existent_resource will, as expected, return a 404. Fixed for v1 and v2. Change-Id: I5406f8ee423d3d5e66c56a26a7009b4f438a7e0f Closes-Bug: #1297362
This commit is contained in:
parent
0e2bc1d2cf
commit
853b5c9b24
|
@ -24,6 +24,8 @@ class API(wsgi.Router):
|
|||
"""WSGI router for Glance v1 API requests."""
|
||||
|
||||
def __init__(self, mapper):
|
||||
reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
|
||||
|
||||
images_resource = images.create_resource()
|
||||
|
||||
mapper.connect("/",
|
||||
|
@ -37,10 +39,22 @@ class API(wsgi.Router):
|
|||
controller=images_resource,
|
||||
action='create',
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect("/images",
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, POST',
|
||||
conditions={'method': ['PUT', 'DELETE', 'HEAD',
|
||||
'PATCH']})
|
||||
mapper.connect("/images/detail",
|
||||
controller=images_resource,
|
||||
action='detail',
|
||||
conditions={'method': ['GET', 'HEAD']})
|
||||
mapper.connect("/images/detail",
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, HEAD',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH']})
|
||||
mapper.connect("/images/{id}",
|
||||
controller=images_resource,
|
||||
action="meta",
|
||||
|
@ -57,6 +71,11 @@ class API(wsgi.Router):
|
|||
controller=images_resource,
|
||||
action="delete",
|
||||
conditions=dict(method=["DELETE"]))
|
||||
mapper.connect("/images/{id}",
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, HEAD, PUT, DELETE',
|
||||
conditions={'method': ['POST', 'PATCH']})
|
||||
|
||||
members_resource = members.create_resource()
|
||||
|
||||
|
@ -68,6 +87,12 @@ class API(wsgi.Router):
|
|||
controller=members_resource,
|
||||
action="update_all",
|
||||
conditions=dict(method=["PUT"]))
|
||||
mapper.connect("/images/{image_id}/members",
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PUT',
|
||||
conditions={'method': ['POST', 'DELETE', 'HEAD',
|
||||
'PATCH']})
|
||||
mapper.connect("/images/{image_id}/members/{id}",
|
||||
controller=members_resource,
|
||||
action="show",
|
||||
|
@ -80,6 +105,11 @@ class API(wsgi.Router):
|
|||
controller=members_resource,
|
||||
action="delete",
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect("/images/{image_id}/members/{id}",
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PUT, DELETE',
|
||||
conditions={'method': ['POST', 'HEAD', 'PATCH']})
|
||||
mapper.connect("/shared-images/{id}",
|
||||
controller=members_resource,
|
||||
action="index_shared_images")
|
||||
|
|
|
@ -28,32 +28,71 @@ class API(wsgi.Router):
|
|||
|
||||
def __init__(self, mapper):
|
||||
custom_image_properties = images.load_custom_properties()
|
||||
reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
|
||||
|
||||
schemas_resource = schemas.create_resource(custom_image_properties)
|
||||
mapper.connect('/schemas/image',
|
||||
controller=schemas_resource,
|
||||
action='image',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/image',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
mapper.connect('/schemas/images',
|
||||
controller=schemas_resource,
|
||||
action='images',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/images',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
mapper.connect('/schemas/member',
|
||||
controller=schemas_resource,
|
||||
action='member',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/member',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
|
||||
mapper.connect('/schemas/members',
|
||||
controller=schemas_resource,
|
||||
action='members',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/members',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
|
||||
mapper.connect('/schemas/task',
|
||||
controller=schemas_resource,
|
||||
action='task',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/task',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
mapper.connect('/schemas/tasks',
|
||||
controller=schemas_resource,
|
||||
action='tasks',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/schemas/tasks',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET',
|
||||
conditions={'method': ['POST', 'PUT', 'DELETE',
|
||||
'PATCH', 'HEAD']})
|
||||
|
||||
images_resource = images.create_resource(custom_image_properties)
|
||||
mapper.connect('/images',
|
||||
|
@ -64,6 +103,13 @@ class API(wsgi.Router):
|
|||
controller=images_resource,
|
||||
action='create',
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect('/images',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, POST',
|
||||
conditions={'method': ['PUT', 'DELETE', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
mapper.connect('/images/{image_id}',
|
||||
controller=images_resource,
|
||||
action='update',
|
||||
|
@ -76,6 +122,11 @@ class API(wsgi.Router):
|
|||
controller=images_resource,
|
||||
action='delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect('/images/{image_id}',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PATCH, DELETE',
|
||||
conditions={'method': ['POST', 'PUT', 'HEAD']})
|
||||
|
||||
image_data_resource = image_data.create_resource()
|
||||
mapper.connect('/images/{image_id}/file',
|
||||
|
@ -86,6 +137,12 @@ class API(wsgi.Router):
|
|||
controller=image_data_resource,
|
||||
action='upload',
|
||||
conditions={'method': ['PUT']})
|
||||
mapper.connect('/images/{image_id}/file',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PUT',
|
||||
conditions={'method': ['POST', 'DELETE', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
image_tags_resource = image_tags.create_resource()
|
||||
mapper.connect('/images/{image_id}/tags/{tag_value}',
|
||||
|
@ -96,12 +153,29 @@ class API(wsgi.Router):
|
|||
controller=image_tags_resource,
|
||||
action='delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect('/images/{image_id}/tags/{tag_value}',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='PUT, DELETE',
|
||||
conditions={'method': ['GET', 'POST', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
image_members_resource = image_members.create_resource()
|
||||
mapper.connect('/images/{image_id}/members',
|
||||
controller=image_members_resource,
|
||||
action='index',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/images/{image_id}/members',
|
||||
controller=image_members_resource,
|
||||
action='create',
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect('/images/{image_id}/members',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, POST',
|
||||
conditions={'method': ['PUT', 'DELETE', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
mapper.connect('/images/{image_id}/members/{member_id}',
|
||||
controller=image_members_resource,
|
||||
action='show',
|
||||
|
@ -110,14 +184,15 @@ class API(wsgi.Router):
|
|||
controller=image_members_resource,
|
||||
action='update',
|
||||
conditions={'method': ['PUT']})
|
||||
mapper.connect('/images/{image_id}/members',
|
||||
controller=image_members_resource,
|
||||
action='create',
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect('/images/{image_id}/members/{member_id}',
|
||||
controller=image_members_resource,
|
||||
action='delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect('/images/{image_id}/members/{member_id}',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PUT, DELETE',
|
||||
conditions={'method': ['POST', 'PATCH', 'HEAD']})
|
||||
|
||||
tasks_resource = tasks.create_resource()
|
||||
mapper.connect('/tasks',
|
||||
|
@ -128,6 +203,13 @@ class API(wsgi.Router):
|
|||
controller=tasks_resource,
|
||||
action='index',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/tasks',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, POST',
|
||||
conditions={'method': ['PUT', 'DELETE', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
mapper.connect('/tasks/{task_id}',
|
||||
controller=tasks_resource,
|
||||
action='get',
|
||||
|
@ -136,5 +218,11 @@ class API(wsgi.Router):
|
|||
controller=tasks_resource,
|
||||
action='delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect('/tasks/{task_id}',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, DELETE',
|
||||
conditions={'method': ['POST', 'PUT', 'PATCH',
|
||||
'HEAD']})
|
||||
|
||||
super(API, self).__init__(mapper)
|
||||
|
|
|
@ -99,6 +99,8 @@ profiler_opts = [
|
|||
]
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(bind_opts)
|
||||
CONF.register_opts(socket_opts)
|
||||
|
@ -447,6 +449,14 @@ class APIMapper(routes.Mapper):
|
|||
return routes.Mapper.routematch(self, url, environ)
|
||||
|
||||
|
||||
class RejectMethodController(object):
|
||||
def reject(self, req, allowed_methods, *args, **kwargs):
|
||||
LOG.debug("The method %s is not allowed for this resource" %
|
||||
req.environ['REQUEST_METHOD'])
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
headers=[('Allow', allowed_methods)])
|
||||
|
||||
|
||||
class Router(object):
|
||||
"""
|
||||
WSGI middleware that maps incoming requests to WSGI apps.
|
||||
|
@ -489,7 +499,7 @@ class Router(object):
|
|||
def __call__(self, req):
|
||||
"""
|
||||
Route the incoming request to a controller based on self.map.
|
||||
If no match, return a 404.
|
||||
If no match, return either a 404(Not Found) or 501(Not Implemented).
|
||||
"""
|
||||
return self._router
|
||||
|
||||
|
@ -498,12 +508,17 @@ class Router(object):
|
|||
def _dispatch(req):
|
||||
"""
|
||||
Called by self._router after matching the incoming request to a route
|
||||
and putting the information into req.environ. Either returns 404
|
||||
or the routed WSGI app's response.
|
||||
and putting the information into req.environ. Either returns 404,
|
||||
501, or the routed WSGI app's response.
|
||||
"""
|
||||
match = req.environ['wsgiorg.routing_args'][1]
|
||||
if not match:
|
||||
return webob.exc.HTTPNotFound()
|
||||
implemented_http_methods = ['GET', 'HEAD', 'POST', 'PUT',
|
||||
'DELETE', 'PATCH']
|
||||
if req.environ['REQUEST_METHOD'] not in implemented_http_methods:
|
||||
return webob.exc.HTTPNotImplemented()
|
||||
else:
|
||||
return webob.exc.HTTPNotFound()
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
|
|
|
@ -22,9 +22,12 @@ import eventlet.patcher
|
|||
import fixtures
|
||||
import gettext
|
||||
import mock
|
||||
import routes
|
||||
import six
|
||||
import webob
|
||||
|
||||
from glance.api.v1 import router as router_v1
|
||||
from glance.api.v2 import router as router_v2
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
|
@ -149,6 +152,62 @@ class RequestTest(test_utils.BaseTestCase):
|
|||
request.headers.pop('Accept-Language')
|
||||
self.assertIsNone(request.best_match_language())
|
||||
|
||||
def test_http_error_response_codes(self):
|
||||
sample_id, member_id, tag_val, task_id = 'abc', '123', '1', '2'
|
||||
|
||||
"""Makes sure v1 unallowed methods return 405"""
|
||||
unallowed_methods = [
|
||||
('/images', ['PUT', 'DELETE', 'HEAD', 'PATCH']),
|
||||
('/images/detail', ['POST', 'PUT', 'DELETE', 'PATCH']),
|
||||
('/images/%s' % sample_id, ['POST', 'PATCH']),
|
||||
('/images/%s/members' % sample_id,
|
||||
['POST', 'DELETE', 'HEAD', 'PATCH']),
|
||||
('/images/%s/members/%s' % (sample_id, member_id),
|
||||
['POST', 'HEAD', 'PATCH']),
|
||||
]
|
||||
api = test_utils.FakeAuthMiddleware(router_v1.API(routes.Mapper()))
|
||||
for uri, methods in unallowed_methods:
|
||||
for method in methods:
|
||||
req = webob.Request.blank(uri)
|
||||
req.method = method
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(405, res.status_int)
|
||||
|
||||
"""Makes sure v2 unallowed methods return 405"""
|
||||
unallowed_methods = [
|
||||
('/schemas/image', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/schemas/images', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/schemas/member', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/schemas/members', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/schemas/task', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/schemas/tasks', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/images', ['PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/images/%s' % sample_id, ['POST', 'PUT', 'HEAD']),
|
||||
('/images/%s/file' % sample_id,
|
||||
['POST', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/images/%s/tags/%s' % (sample_id, tag_val),
|
||||
['GET', 'POST', 'PATCH', 'HEAD']),
|
||||
('/images/%s/members' % sample_id,
|
||||
['PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/images/%s/members/%s' % (sample_id, member_id),
|
||||
['POST', 'PATCH', 'HEAD']),
|
||||
('/tasks', ['PUT', 'DELETE', 'PATCH', 'HEAD']),
|
||||
('/tasks/%s' % task_id, ['POST', 'PUT', 'PATCH', 'HEAD']),
|
||||
]
|
||||
api = test_utils.FakeAuthMiddleware(router_v2.API(routes.Mapper()))
|
||||
for uri, methods in unallowed_methods:
|
||||
for method in methods:
|
||||
req = webob.Request.blank(uri)
|
||||
req.method = method
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(405, res.status_int)
|
||||
|
||||
"""Makes sure not implemented methods return 501"""
|
||||
req = webob.Request.blank('/schemas/image')
|
||||
req.method = 'NonexistentMethod'
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(501, res.status_int)
|
||||
|
||||
|
||||
class ResourceTest(test_utils.BaseTestCase):
|
||||
|
||||
|
|
Loading…
Reference in New Issue