$30 off During Our Annual Pro Sale. View Details »

Rack and Middleware

Tim Uruski
February 03, 2015

Rack and Middleware

Introduction to Rack and Middleware for YYC Ruby meetup, Feb 2015.

Tim Uruski

February 03, 2015
Tweet

More Decks by Tim Uruski

Other Decks in Programming

Transcript

  1. &
    Rack
    Middleware

    View Slide

  2. What is Rack?

    View Slide

  3. Rack is both
    a protocol and a gem

    View Slide

  4. Created in 2007
    By Christian Neukirchen
    (for reference, Rails was created in 2004)

    View Slide

  5. MyApp
    Thin
    Unicorn
    Puma
    Passenger
    WEBrick
    Internets
    Rails
    Sinatra
    Lotus
    Server Framework
    Network Application
    RACK

    View Slide

  6. assert app.respond_to? :call
    status, headers, body = app.call(env)
    assert status.to_i >= 100
    assert headers.is_a? Hash
    assert body.respond_to? :each

    View Slide

  7. class MyApp
    def call(env)
    [200, {}, ['Hello world']]
    end
    end

    View Slide

  8. Proc.new do |env|
    [200,{},['Hello world']]
    end

    View Slide

  9. ->(env) { [200,{},['Hello world']] }

    View Slide

  10. env and CGI

    View Slide

  11. curl localhost:8080/hello/world?foo=bar

    View Slide

  12. GATEWAY_INTERFACE: "CGI/1.1"
    HTTP_ACCEPT: "*/*"
    HTTP_HOST: "localhost:8080"
    HTTP_USER_AGENT: "curl/7.37.1"
    HTTP_VERSION: "HTTP/1.1"
    PATH_INFO: "/hello/world"
    QUERY_STRING: "foo=bar"
    REMOTE_ADDR: "::1"
    REMOTE_HOST: "localhost"
    REQUEST_METHOD: "GET"
    REQUEST_PATH: "/hello/world"
    REQUEST_URI: "http://localhost:8080/hello/world?foo=bar"
    SCRIPT_NAME: ""
    SERVER_NAME: "localhost"
    SERVER_PORT: "8080"
    SERVER_PROTOCOL: "HTTP/1.1"
    SERVER_SOFTWARE: "WEBrick/1.3.1 (Ruby/2.1.3/2014-09-19)"

    View Slide

  13. GATEWAY_INTERFACE: "CGI/1.1"
    HTTP_ACCEPT: "*/*"
    HTTP_HOST: "localhost:8080"
    HTTP_USER_AGENT: "curl/7.37.1"
    HTTP_VERSION: "HTTP/1.1"
    PATH_INFO: "/hello/world"
    QUERY_STRING: "foo=bar"
    REMOTE_ADDR: "::1"
    REMOTE_HOST: "localhost"
    REQUEST_METHOD: "GET"
    REQUEST_PATH: "/hello/world"
    REQUEST_URI: "http://localhost:8080/hello/world?foo=bar"
    SCRIPT_NAME: ""
    SERVER_NAME: "localhost"
    SERVER_PORT: "8080"
    SERVER_PROTOCOL: "HTTP/1.1"
    SERVER_SOFTWARE: "WEBrick/1.3.1 (Ruby/2.1.3/2014-09-19)"

    View Slide

  14. GATEWAY_INTERFACE: "CGI/1.1"
    HTTP_ACCEPT: "*/*"
    HTTP_HOST: "localhost:8080"
    HTTP_USER_AGENT: "curl/7.37.1"
    HTTP_VERSION: "HTTP/1.1"
    PATH_INFO: "/hello/world"
    QUERY_STRING: "foo=bar"
    REMOTE_ADDR: "::1"
    REMOTE_HOST: "localhost"
    REQUEST_METHOD: "GET"
    REQUEST_PATH: "/hello/world"
    REQUEST_URI: "http://localhost:8080/hello/world?foo=bar"
    SCRIPT_NAME: ""
    SERVER_NAME: "localhost"
    SERVER_PORT: "8080"
    SERVER_PROTOCOL: "HTTP/1.1"
    SERVER_SOFTWARE: "WEBrick/1.3.1 (Ruby/2.1.3/2014-09-19)"

    View Slide

  15. rack.errors: #>
    rack.hijack: #@lib/rack/handler/webrick.rb:77 (lambda)>
    rack.hijack?: true
    rack.hijack_io: nil
    rack.input: #
    rack.multiprocess: false
    rack.multithread: true
    rack.run_once: false
    rack.url_scheme: "http"
    rack.version: [1, 3]

    View Slide

  16. rack.errors: #>
    rack.hijack: #@lib/rack/handler/webrick.rb:77 (lambda)>
    rack.hijack?: true
    rack.hijack_io: nil
    rack.input: #
    rack.multiprocess: false
    rack.multithread: true
    rack.run_once: false
    rack.url_scheme: "http"
    rack.version: [1, 3]

    View Slide

  17. rack.errors: #>
    rack.hijack: #@lib/rack/handler/webrick.rb:77 (lambda)>
    rack.hijack?: true
    rack.hijack_io: nil
    rack.input: #
    rack.multiprocess: false
    rack.multithread: true
    rack.run_once: false
    rack.url_scheme: "http"
    rack.version: [1, 3]

    View Slide

  18. middleware

    View Slide

  19. Handler Middleware Middleware App
    Internets

    View Slide

  20. Handler Middleware Middleware
    Internets
    Request
    Request
    Request
    Request
    Request
    Internets App

    View Slide

  21. Handler Middleware Middleware
    Request
    Response
    Response
    Response
    Response
    Internets App

    View Slide

  22. Handler Middleware
    Internets
    Request
    Request
    Request
    Request
    Internets
    Response
    Response
    Response
    Middleware App

    View Slide

  23. qux = ->(env){ [200,{},['Hello world']] }
    bar = ->(env){ qux.call(env) }
    foo = ->(env){ bar.call(env) }
    run foo

    View Slide

  24. class Middleware
    def initialize(app)
    @app = app
    end
    def call(env)
    @app.call(env)
    end
    end

    View Slide

  25. class Reverse < Middleware
    def call(env)
    status, headers, body = super
    body.each(&:reverse!)
    [status, headers, body]
    end
    end
    use Reverse
    use Reverse
    run ->(env){ [200,{},['Hello world']] }

    View Slide

  26. Rack::Builder
    and config.ru

    View Slide

  27. $ rackup config.ru

    View Slide

  28. if config.end_with?('ru')
    Rack::Builder.new {

    }.to_app
    end

    View Slide

  29. require_relative 'lib/my_app'
    use Rack::CommonLogger
    use Rack::Session::Cookie
    run MyApp.new

    View Slide

  30. map '/admin' do
    use Authorization::Basic
    run Admin::App.new
    end
    map '/api' do
    use Authorization::Token
    run Api::App.new
    end

    View Slide

  31. warmup do |app|
    client = Rack::MockRequest.new(app)
    client.get '/cached_resource'
    end

    View Slide

  32. rackup greeter.rb

    View Slide

  33. if config.end_with?('rb')
    require config
    app_name = File.basename(config, '.rb')
    app = Object.const_get(app_name.capitalize)
    run app
    end

    View Slide

  34. class Greeter
    def self.call(env)
    params = Rack::Request.new(env).params
    name = params.fetch('name', 'unknown')
    [200, {}, ["Greetings, #{name}"]]
    end
    end

    View Slide

  35. # greeter.rb -> Greeter
    # my_app.rb -> My_app

    View Slide

  36. Request, Response,
    BodyProxy, and Utils

    View Slide

  37. def call(env)
    request = Rack::Request.new(env)
    request.request_method?
    request.post?
    request.get?
    request.params['query']
    request.update_params['query', 'foobar']
    request.ip
    end

    View Slide

  38. def call(env)
    response = @app.call(env)
    response = Rack::Response.new(response)
    response['Etag'] = 'abc123'
    response.redirect('/faq')
    response.set_cookie('user_id', 123)
    response.write 'Hello world'
    end

    View Slide

  39. def call(env)
    status, headers, body = @app.call
    body = BodyProxy.new(body) do
    # Ensure something happens after
    # body.close is called, eg.
    DB.connection.close
    end
    [status, headers, body]
    end

    View Slide

  40. include Rack::Utils
    parse_query 'foo=bar&foo=qux'
    #=> {'foo' => ['bar','qux']}
    parse_nested_query 'foo[]=bar&foo[]=qux'
    #=> {'foo' => ['bar','qux']}
    parse_nested_query 'foo[x]=bar&foo[y]=qux'
    #=> {'foo' => {'x' => 'bar', 'y' => 'qux'}}

    View Slide

  41. include Rack::Utils
    build_query {foo: ['bar','qux']}
    #=> 'foo=bar&foo=qux'
    build_nested_query {foo: ['bar', 'qux']}
    #=> 'foo[]=bar&foo[]=qux'
    build_nested_query {foo: {x: 'bar', y: 'qux'}}
    #=> 'foo[x]=bar&foo[y]=qux'

    View Slide

  42. Callbacks
    Cascade
    Chunked
    CommonCookies
    CommonLogger
    ConditionalGet
    Config
    ContentLength
    ContentType
    Context
    CookieJar
    Cookies
    DeflateStream
    Deflater
    Deflater
    Deflect
    Directory
    ETag
    ErrorWrapper
    Evil
    ExpectationCascade
    File
    ForwardRequest
    GarbageCollector
    GzipStream
    HeaderHash
    HijackWrapper
    HostMeta
    InputWrapper
    InvalidParameterError
    JSONP
    KeySpaceConstrainedParams
    LighttpdScriptNameFix
    Lint
    LintError
    Lobster
    Locale
    Lock
    Logger
    MD5
    MailExceptions
    MethodOverride
    NullLogger
    ParameterTypeError
    PostBodyContentTypeParser
    Printout
    ProcTitle
    Profiler
    Recursive
    Reloader
    Request
    ResponseHeaders
    RouteExceptions
    Runtime
    Sendfile
    SessionHash
    rack-contrib
    https://github.com/rack/rack-contrib

    View Slide

  43. Rails

    View Slide

  44. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use ActiveSupport::Cache::Strategy::LocalCache::Middleware
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run Rails.application.routes
    $ rake middleware

    View Slide

  45. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use ActiveSupport::Cache::Strategy::LocalCache::Middleware
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run Rails.application.routes
    $ rake middleware

    View Slide

  46. Rails.application.routes
    #=> ActionDispatch::Routing::RouteSet
    def initialize
    @set = Journey::Routes.new
    @router = Journey::Router.new @set
    end
    def call(env)
    req = ActionDispatch::Request.new(env)
    @router.serve(req)
    end

    View Slide

  47. module ActionDispatch
    class Request < Rack::Request
    def initialize(env)
    super
    # ...
    end
    end
    end

    View Slide

  48. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use ActiveSupport::Cache::Strategy::LocalCache::Middleware
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run Rails.application.routes
    $ rake middleware

    View Slide

  49. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use ActiveSupport::Cache::Strategy::LocalCache::Middleware
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run Rails.application.routes
    $ rake middleware

    View Slide

  50. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use ActiveSupport::Cache::Strategy::LocalCache::Middleware
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run Rails.application.routes
    $ rake middleware

    View Slide

  51. More details about Rails on Rack:
    http://guides.rubyonrails.org/rails_on_rack.html

    View Slide

  52. rack.hijack

    View Slide

  53. env['rack.hijack?'] #=> true/false
    env['rack.hijack'] #=> .call
    env['rack.hijack_io'] #=> IO/Socket

    View Slide

  54. app = Proc.new do |env|
    io = env['rack.hijack'].call
    begin
    io.write("Status: 200\r\n")
    io.write("Connection: close\r\n")
    io.write("Content-Type: text/plain\r\n")
    io.write("\r\n")
    10.times do |i|
    io.write("Line #{i + 1}!\n")
    io.flush
    sleep 0.1
    end
    ensure
    io.close
    end
    end

    View Slide

  55. app = Proc.new do |env|
    io = env['rack.hijack'].call
    begin
    io.write("Status: 200\r\n")
    io.write("Connection: close\r\n")
    io.write("Content-Type: text/plain\r\n")
    io.write("\r\n")
    10.times do |i|
    io.write("Line #{i + 1}!\n")
    io.flush
    sleep 0.1
    end
    ensure
    io.close
    end
    end

    View Slide

  56. app = Proc.new do |env|
    hijack = ->(io) {
    10.times do |n|
    io.write("Line #{n + 1}\n")
    io.flush
    sleep 0.1
    end
    io.close
    }
    headers = {'rack.hijack' => hijack}
    [200, headers, nil]
    }

    View Slide

  57. • Supported by Passenger
    • Supported by Unicorn (for responses < 30s)
    • Supported by Puma
    • WEBrick only supports partial hijacking
    • Not supported by Thin (maybe?)

    View Slide

  58. MyApp
    Server
    Internets Framework
    Server Framework
    Network Application
    HIJACK
    Rack

    View Slide

  59. The future of Rack?
    https://github.com/tenderlove/the_metal
    https://gist.github.com/raggi/11c3491561802e573a47
    https://github.com/Wardrop/Rack-Next

    View Slide

  60. Exercise
    https://github.com/yycruby/middleware_exercise
    [email protected]:yycruby/middleware_exercise.git

    View Slide