Sinatra: Strange behavior in modular application

Created on 1 Jan 2017  路  4Comments  路  Source: sinatra/sinatra

Step:

  1. Add "error 403 do ... end" in application controller , and return {code: status, message: body}
  2. Add halt 403, 'a' in AController
  3. Add halt 403, 'b' in BController
  4. Add halt 403, 'c' in CController

  5. Try request url '/a', got
    {"code":403,"message":["Oooooops, A"]}

  6. Try request url '/b/, got
    {"code":403,"message":"{\"code\":403,\"message\":\"Oooooops, B\"}"}
  7. Try request url '/c', got
    {"code":403,"message":["{\"code\":403,\"message\":[\"{\\\"code\\\":403,\\\"message\\\":[\\\"Oooooops, C\\\"]}\"]}"]}

That's the problem, Why response like a "Matryoshka doll"?
How many times body repeat is depend on 'use' order in config.ru

Here is sample code, run it with bundle install then rackup -p 9192
sample_code.tar.gz

Most helpful comment

Your error handler is fine, but the way you're structuring your controllers and middleware stack is causing the nesting. All of your controllers inherit the error handler from ApplicationController and apply it on error 403.

There are a few different ways to tackle this architectural problem. To do it like you're trying to do it, you can put your error-handling logic in its own middleware and include it at the top of the stack, like so:

# test.ru
require 'json'
require 'sinatra/base'

class ErrorController < Sinatra::Base    
  error 403 do
    { code: status, message: body.join }.to_json
  end
end

class ApplicationController < Sinatra::Base    
  get '/' do
    'hello'
  end
end

class AController < ApplicationController
  get '/a' do
    halt 403, 'Oooooops, A'
  end
end

class BController < ApplicationController
  get '/b' do
    halt 403, 'Oooooops, B'
  end
end

class CController < ApplicationController
  get '/c' do
    halt 403, 'Oooooops, C'
  end
end

use ErrorController
use AController
use BController
use CController
run ApplicationController

Another option is to use Rack::URLMap instead of (or alongside) a middleware stack, and namespace your controllers:

# test.ru
require 'json'
require 'sinatra/base'

class ApplicationController < Sinatra::Base    
  error 403 do
    { code: status, message: body.join }.to_json
  end

  get '/' do
    'hello'
  end
end

class AController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, A'
  end
end

class BController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, B'
  end
end

class CController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, C'
  end
end

run Rack::URLMap.new(
  '/a' => AController,
  '/b' => BController,
  '/c' => CController,
  '/' => ApplicationController
)

Now the routing is done entirely in Rack before the requests hit any of the Sinatra controllers.

These are just some options. As you're getting started, I would recommend you continue learning Ruby and dig into the philosophy of Rack a bit more. It's useful to keep in mind that Sinatra is a relatively thin DSL on top of Rack and isn't very opinionated.

All 4 comments

You are seeing this behavior because you are using the three controllers as middleware. This is exactly the expected behavior: the inner-most Rack application processes the request and passes the response to the second inner-most Rack application, and so forth and so on. Because all the controllers inherit the same error-handler, they all apply the same logic to the response.

If you can explain what you are trying to do, we might be able to help you figure out the best way to do it.

@mwpastore Maybe i misunderstood something, I just want use my own error page template.

http://stackoverflow.com/questions/36127041/in-ruby-sinatra-how-to-halt-with-an-erb-template-and-error-message

The answer in this link is the right way? I'm a beginner in Ruby...

Your error handler is fine, but the way you're structuring your controllers and middleware stack is causing the nesting. All of your controllers inherit the error handler from ApplicationController and apply it on error 403.

There are a few different ways to tackle this architectural problem. To do it like you're trying to do it, you can put your error-handling logic in its own middleware and include it at the top of the stack, like so:

# test.ru
require 'json'
require 'sinatra/base'

class ErrorController < Sinatra::Base    
  error 403 do
    { code: status, message: body.join }.to_json
  end
end

class ApplicationController < Sinatra::Base    
  get '/' do
    'hello'
  end
end

class AController < ApplicationController
  get '/a' do
    halt 403, 'Oooooops, A'
  end
end

class BController < ApplicationController
  get '/b' do
    halt 403, 'Oooooops, B'
  end
end

class CController < ApplicationController
  get '/c' do
    halt 403, 'Oooooops, C'
  end
end

use ErrorController
use AController
use BController
use CController
run ApplicationController

Another option is to use Rack::URLMap instead of (or alongside) a middleware stack, and namespace your controllers:

# test.ru
require 'json'
require 'sinatra/base'

class ApplicationController < Sinatra::Base    
  error 403 do
    { code: status, message: body.join }.to_json
  end

  get '/' do
    'hello'
  end
end

class AController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, A'
  end
end

class BController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, B'
  end
end

class CController < ApplicationController
  get '/' do
    halt 403, 'Oooooops, C'
  end
end

run Rack::URLMap.new(
  '/a' => AController,
  '/b' => BController,
  '/c' => CController,
  '/' => ApplicationController
)

Now the routing is done entirely in Rack before the requests hit any of the Sinatra controllers.

These are just some options. As you're getting started, I would recommend you continue learning Ruby and dig into the philosophy of Rack a bit more. It's useful to keep in mind that Sinatra is a relatively thin DSL on top of Rack and isn't very opinionated.

Thank you for the code and advice. I will continue learn Ruby and it's philosophy in depth.

This is my first Ruby + Sinatra project after some fragmented time learning. I think i should forget some experience in other language, just think it in the 'Ruby way'.

BTW: Sorry for my bad English, hope you can understand it.

Was this page helpful?
0 / 5 - 0 ratings