Handling 405's in Sinatra - ruby

In Sinatra if I create a simple endpoint such as:
post '/users' do
'posted'
end
curl -v -X GET http://localhost:8080/users returns a 404, when I would expect a 405.
I've looked through the documentation but not found anything. Before digging through the source code, does anyone know how to handle and return 405's in Sinatra? Sort of like the not found method:
not_found do
'Not found - ' + request.path
end

You got an 404 because sinatra can't find a get route /users. If you want return a custom error you may look at Halt
throw a error
The you can return a 405 on get /users like this:
get "/users" do
halt 405
end
For catch multiple http verbs at once you can use multi route
require 'sinatra'
require "sinatra/multi_route"
route :get, :post, '/foo' do
# "GET" or "POST"
p request.env["REQUEST_METHOD"]
end
# Or for module-style applications
class MyApp < Sinatra::Base
register Sinatra::MultiRoute
route :get, :post, '/foo' do
# ...
end
end
source
handle errors
If you want handle error codes in sinatra you can simple do for 404 errors:
not_found do
'This is nowhere to be found.'
end
In your case handle a 405 error:
error 405 do
'Access forbidden'
end

Looks like it's not possible without repeating yourself a lot. From looking through the Sinatra source code, the hash of routes has the verb as the key: https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1513
It then looks up a route using that verb:
https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L943
Which is not ideal and I would consider a weakness/bug in the framework as throwing a 405 when a verb isn't allowed is standard HTTP Spec.
I'll maybe raise an issue see what the contributors say. Ideally it would store routes by their url first, and then check the appropriate verb can be executed for a given url. This would make handling something as standard as a 405, much easier.
In fact I found an issue raised on github for the above:
https://github.com/sinatra/sinatra/issues/24
As mentioned nearer the bottom, it's not currently handled and something they may work on for v2.0

Related

How do I add custom logging for which route is satisfying a request in Sinatra?

I'm working on a Sinatra app that has a bunch of routes of all sorts. I'd like to add some custom logging that logs the params of the get or post call that ends up generating the response for the request. I realize I could subclass the get/post definition to wrap the block with a logging call. But I suspect there is a more appropriate approach.
You can use Sinatra's before hook in your controller, and print out some information contained on the request
before do
if request.request_method == :get || request.request_method == :post
puts request.path_info, params.inspect # check out the request variable for more info you might like to ouput
end
end

override not_found sinatra application

sinatra (= 1.4.5)
I want to render a custom 404 message for specific routes, in this case, the text 'Not found'
class App < Sinatra::Application
not_found do
'You suck'
end
get '/404page' do
halt 404, 'Not found'
end
end
I have found that no matter what I do, the text from the not_found block is invoked any time response status is set to 404, when all I really want is the text from the halt.
May be a duplicate of Override Sinatra default NotFound error page but I have not been able to find the soution.
Overriding error Sinatra::NotFound seems to be a better choice to handle 404 when a specific route is not found.
error Sinatra::NotFound do
'You suck'
end
get '/404page' do
halt 404, 'Not found'
end
You can pass a body to not_found, so your get request would just look something like this.
get '/404page' do
not_found('Not found')
end
This will call halt 404, 'Not found' via the not_found method.
More often than not this is what you will need; if this does not suit your needs then you would need to consider overriding the Sinatra route_missing method, which I have included below.
# No matching route was found or all routes passed. The default
# implementation is to forward the request downstream when running
# as middleware (#app is non-nil); when no downstream app is set, raise
# a NotFound exception. Subclasses can override this method to perform
# custom route miss logic.
def route_missing
if #app
forward
else
raise NotFound
end
end

How to read POST data in rack request

When I run the curl command
curl -v -H "Content-type: application/json" -X POST -d '{"name":"abc", "id":"12", "subject":"my subject"}' http://localhost:9292
to send a POST request with data to my Rack application, my code prints out {}. That is coming from puts req.POST() in the code below.
Why does it print out {} instead of the POST data? And how do I correctly access the POST data in my Rack application?
require 'json'
class Greeter
def call(env)
req = Rack::Request.new(env)
if req.post?
puts req.POST()
end
[200, {"Content-Type" => "application/json"}, [{x:"Hello World!"}.to_json]]
end
end
run Greeter.new
From reading the docs for POST, looks like it is giving you parsed data based on other content types. If you want to process "application/json", you probably need to
JSON.parse( req.body.read )
instead. To check this, try
puts req.body.read
where you currently have puts req.POST.
req.body is an I/O object, not a string. See the body documentation and view the source. You can see that this is in fact the same as mudasobwa's answer.
Note that other code in a Rack application may expect to read the same I/O, such as the param parsers in Sinatra or Rails. To ensure that they see the same data and not get an error, you may want to call req.body.rewind, possibly both before and after reading the request body in your code. However, if you are in such a situation, you might instead consider whether your framework has options to process JSON directly via some option on the controller or request content-type handler declaration etc - most likely there will be an option to handle this kind of request within the framework.
Try:
env['rack.input'].read
I found it in "How to receive a JSON object with Rack" and, though it still sounds weird to me, it likely works.
You can try:
req.params
Hope this can help you.

Sinatra app that redirects POST/GET requests with parameters

I'm migrating servers but unfortunately the old server IP is hardcoded inside my iPhone app. Obviously I'm going to submit an update that sets the API endpoint to my new server, but in the meantime I need to setup an app on the old server that redirects all the requests to the new server. I've heard Sinatra would be perfect for this.
require 'sinatra'
get "/foo/bar" do
redirect "http://new-server.com/foo/bar", 303
end
post "/foo/bar" do
redirect "http://new-server.com/foo/bar", 303
end
The problem is that these do not forward the GET or POST parameters along with the request. I read on the Sinatra doc that you can do that by putting them in the URL directly (works for GET requests), or by setting session variables.
Is manually parsing and formatting the GET params to put them back into the redirect URL the only way to go for GET redirects? How are you supposed to forward POST parameters?
For GET requests, use request.fullpath or request.query_string. For POST request, use status code 307, so the subsequent request will be a POST with the same parameters.
helpers do
def new_url
"http://new-server.com" + request.fullpath
end
end
get "/foo/bar" do
redirect new_url
end
post "/foo/bar" do
redirect new_url, 307
end
I would overload the Hash class in lib/overload_hash.rb, like so:
class Hash
def to_url_params
elements = []
keys.size.times do |i|
elements << "#{keys[i]}=#{values[i]}"
end
elements.join('&')
end
end
EDIT (Better solution using net / http)
Place a require "lib/overload_hash", require "net/http" and require "uri" under require 'sinatra'. The following example can be translated into GET easily.
post '/foo/bar' do
uri = URI.parse("http://example.com/search")
response = Net::HTTP.post_form(uri, params.to_ur_params)
response
end

How do I make a uber-simple API wrapper in Ruby?

I'm trying to use a super simple API from is.gd:
http://is.gd/api.php?longurl=http://www.example.com
Which returns a response header "HTTP/1.1 200 OK" if the URL was shortened as expected, or "HTTP/1.1 500 Internal Server Error" if there was any problem that prevented this. Assuming the request was successful, the body of the response will contain only the new shortened URL
I don't even know where to begin or if there are any available ruby methods to make sending and receiving of these API requests frictionless. I basically want to assign the response (the shortened url) to a ruby object.
How would you do this? Thanks in advance.
Super simple:
require 'open-uri'
def shorten(url)
open("http://is.gd/api.php?longurl=#{url}").read
rescue
nil
end
open-uri is part of the Ruby standard library and (among other things) makes it possible to do HTTP requests using the open method (which usually opens files). open returns an IO, and calling read on the IO returns the body. open-uri will throw an exception if the server returns a 500 error, and in this case I'm catching the exception and return nil, but if you want you can let the exception bubble up to the caller, or raise another exception.
Oh, and you would use it like this:
url = "http://www.example.com"
puts "The short version of #{url} is #{shorten(url)}"
I know you already got an answer you accepted, but I still want to mention httparty because I've made very good experiences wrapping APIs (Delicious and Github) with it.

Resources