From 7e13c959f3a624a49b362ae7ec2fc1ff6d7af5e4 Mon Sep 17 00:00:00 2001 From: Caleb Crane Date: Thu, 27 Sep 2012 18:44:24 +0900 Subject: [PATCH 1/2] adds 405 response for methods not supported on a resource instead of 404 For every resource an 'OPTIONS' route that returns an HTTP 204 response will be added. Also a route that will return an HTTP 405 response for any HTTP method that the resource cannot handle will be added. The behavior up to now has been to return an HTTP 404 response if the resource doesn't respond to the request's HTTP method. --- lib/grape/api.rb | 37 +++++++++++++++++++++++++++++++++++++ spec/grape/api_spec.rb | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 2b7a5e501a..8ff0a13f88 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -414,6 +414,7 @@ def initialize self.class.endpoints.each do |endpoint| endpoint.mount_in(@route_set) end + add_head_not_allowed_methods @route_set.freeze end @@ -422,5 +423,41 @@ def call(env) end reset! + + private + + # For every resource add a 'OPTIONS' route that returns an HTTP 204 response + # with a list of HTTP methods that can be called. Also add a route that + # will return an HTTP 405 response for any HTTP method that the resource + # cannot handle. + def add_head_not_allowed_methods + allowed_methods = Hash.new{|h,k| h[k] = [] } + resources = self.class.endpoints.map do |endpoint| + endpoint.options[:app] && endpoint.options[:app].respond_to?(:endpoints) ? + endpoint.options[:app].endpoints.map(&:routes) : + endpoint.routes + end + resources.flatten.each do |route| + allowed_methods[route.route_compiled] << route.route_method + end + + allowed_methods.each do |path_info, methods| + allow_header = (["OPTIONS"] | methods).join(", ") + unless methods.include?("OPTIONS") + @route_set.add_route( proc { [204, { 'Allow' => allow_header }, []]}, { + :path_info => path_info, + :request_method => "OPTIONS" + }) + end + not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - methods + not_allowed_methods.each do |bad_method| + @route_set.add_route( proc { [405, { 'Allow' => allow_header }, []]}, { + :path_info => path_info, + :request_method => bad_method + }) + end + end + end + end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 03aecedb1f..c3a9ad7376 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -301,8 +301,8 @@ def app; subject end send(verb, '/example') last_response.body.should eql verb == 'head' ? '' : verb # Call it with a method other than the properly constrained one. - send(verbs[(verbs.index(verb) + 1) % verbs.size], '/example') - last_response.status.should eql 404 + send(used_verb = verbs[(verbs.index(verb) + 1) % verbs.size], '/example') + last_response.status.should eql used_verb == 'options' ? 204 :405 end end @@ -315,6 +315,36 @@ def app; subject end last_response.status.should eql 201 last_response.body.should eql 'Created' end + + it 'should return a 405 for an unsupported method' do + subject.get 'example' do + "example" + end + put '/example' + last_response.status.should eql 405 + last_response.body.should eql '' + end + + specify '405 responses should include an Allow header specifying supported methods' do + subject.get 'example' do + "example" + end + subject.post 'example' do + "example" + end + put '/example' + last_response.headers['Allow'].should eql 'OPTIONS, GET, POST' + end + + it 'should add an OPTIONS route that returns a 204 and an Allow header' do + subject.get 'example' do + "example" + end + options '/example' + last_response.status.should eql 204 + last_response.body.should eql '' + last_response.headers['Allow'].should eql 'OPTIONS, GET' + end end describe 'filters' do From eaeb55e25d29471c2699b55159a870d616aa3362 Mon Sep 17 00:00:00 2001 From: Caleb Crane Date: Sun, 30 Sep 2012 10:17:06 +0900 Subject: [PATCH 2/2] updates changelog and readme with HTTP 405 (Method Not Allowed) response details --- CHANGELOG.markdown | 1 + README.markdown | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 97646860c4..1b8dc01a4c 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -20,6 +20,7 @@ Fixes * [#181](https://github.com/intridea/grape/pull/181): Fix: Corrected JSON serialization of nested hashes containing `Grape::Entity` instances - [@benrosenblum](https://github.com/benrosenblum). * [#203](https://github.com/intridea/grape/pull/203): Added a check to `Entity#serializable_hash` that verifies an entity exists on an object - [@adamgotterer](https://github.com/adamgotterer). * [#208](https://github.com/intridea/grape/pull/208): `Entity#serializable_hash` must also check if attribute is generated by a user supplied block - [@ppadron](https://github.com/ppadron). +* [#252](https://github.com/intridea/grape/pull/252): Resources that don't respond to a requested HTTP method return 405 (Method Not Allowed) instead of 404 (Not Found) [@simulacre](https://github.com/simulacre) 0.2.1 (7/11/2012) ================= diff --git a/README.markdown b/README.markdown index d4ad3b6e3c..5afa5861f2 100644 --- a/README.markdown +++ b/README.markdown @@ -411,6 +411,50 @@ redirect "/new_url" redirect "/new_url", :permanent => true ``` +## Allowed Methods + +When you add a route for a resource, a route for the HTTP OPTIONS +method will also be added. The response to an OPTIONS request will +include an Allow header listing the supported methods. + +``` ruby +class API < Grape::API + get '/counter' do + { :counter => Counter.count } + end + + params do + requires :value, :type => Integer, :desc => 'value to add to counter' + end + put '/counter' do + { :counter => Counter.incr(params.value) } + end +end +``` + +``` shell +curl -v -X OPTIONS http://localhost:3000/counter + +> OPTIONS /counter HTTP/1.1 +> +< HTTP/1.1 204 No Content +< Allow: OPTIONS, GET, PUT +``` + + +If a request for a resource is made with an unsupported HTTP method, an +HTTP 405 (Method Not Allowed) response will be returned. + +``` shell +curl -X DELETE -v http://localhost:3000/counter/ + +> DELETE /counter/ HTTP/1.1 +> Host: localhost:3000 +> +< HTTP/1.1 405 Method Not Allowed +< Allow: OPTIONS, GET, PUT +``` + ## Raising Exceptions You can abort the execution of an API method by raising errors with `error!`.