Rack入門 Rack Middleware編 (3/3)

[前回](https://qiita.com/nishio-dens/items/852d7604b34d20514a70) は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。

## 目次

1. [Rack入門 概念編(1/3)](https://qiita.com/nishio-dens/items/e293f15856d849d3862b
)
1. [Rack入門 Rack Application編 (2/3)](https://qiita.com/nishio-dens/items/852d7604b34d20514a70)
1. [本記事] [Rack入門 Rack Middleware編 (3/3)](https://qiita.com/nishio-dens/items/8011842f50995f46eafe)


## Rack Middlewareとは

はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。

今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。

- Rack Application
  - 前回学んだ、callメソッドを持つオブジェクトのことです
  - StatusCode・Headers・Bodyの3つをレスポンスとして返します
  - Rack Endpointとも呼ばれます

- Rack Middleware
  - 今回学ぶものです
  - Rack Middlewareは**callメソッドを持つclass**である必要があります
  - Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います

Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。

![rack03_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12701/5aa24ee3-0176-3f34-dccb-cc41a7419fb2.png)


## Hello Rack Middleware

Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。

```ruby
class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

# Rack Middlewareは以下条件を満たす必要がある
#   - classであること
#   - initializeでappを受け取ること
#   - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件)
class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
run App.new
```

Rack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。

```
$ curl http://localhost:9292/

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
Hello Rack Middleware!
HELLO Rack Endpoint!

* Connection #0 to host localhost left intact
* Closing connection 0
```

`Hello Rack Middleware!` という文字列がBodyに追加されていますね。

Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。

```ruby
class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

class AnotherMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Another Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
use AnotherMiddleware
run App.new
```

レスポンスは以下のとおりです。

```ruby
Hello Rack Middleware!
Another Middleware!
HELLO Rack Endpoint!
```

## Content-Lengthを追加するMiddleware

もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。

```ruby
class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]]
  end
end

class ContentLengthMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body_size = 0

    body.each do |b|
      body_size += b.bytesize
    end
    headers["Content-Length"] = body_size.to_s

    [status, headers, body]
  end
end

use ContentLengthMiddleware

run App.new
```

アクセスしてみましょう。

```
$ curl -v http://localhost:9292/ 

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 17
<
* Connection #0 to host localhost left intact
HELLO WORLD!Hello* Closing connection 0
```

ちゃんとContent-Lengthが設定されていますね。


## Rackにあらかじめ用意されているRack Middleware

Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。

- Rack::ContentLength
  - https://github.com/rack/rack/blob/master/lib/rack/content_length.rb
  - Bodyをみて、Content-Lengthヘッダーを設定します

- Rack::Deflater 
  - https://github.com/rack/rack/blob/master/lib/rack/deflater.rb
  - レスポンスを圧縮します

- Rack::ETag
  - https://github.com/rack/rack/blob/master/lib/rack/etag.rb
  - Etagを処理します

- Rack::Session::Cookie
  - https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb
  - cookieベースのSession管理を行います

- Rack::Reloader
  - https://github.com/rack/rack/blob/master/lib/rack/reloader.rb
  - リクエストが来た際に、実行中のRubyファイルが更新されていたら自動でリロードします(開発時に利用するもの)

その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。



## Railsで使われているRack Middleware

Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。

```
$ rails new racktest

...省略...


$ cd racktest

$ bin/rake middleware

Running via Spring preloader in process 10795
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Racktest::Application.routes
```

いくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。


## まとめ

- Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
- Middlewareはcallメソッドを実装したclassである必要がある

## Rackをより理解するためには

全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。

よりRackを理解するための参考資料を、以下に記載しておきます。

- Rack本家のExample
  - https://github.com/rack/rack/blob/master/lib/rack/lobster.rb
  - アクセスするとロブスターを返します。`bundle exec ruby lib/rack/lobster` で起動できます
  - `Rack::Request`, `Rack::Response`を使って処理をラップしているので少し複雑ですが、基本的な考え方は今回学んだとおりです
  - `Response#finish` は https://github.com/rack/rack/blob/master/lib/rack/response.rb#L66 のとおりで、StatusCode/Headers/Body のペアを返します
  -  (Paste has a Pony って、なんだろう...)

- 次世代のRackやWSGIを考えてみる
  - https://qiita.com/kwatch/items/67657fef43666479bb99
  - Rackの問題点について言及されています。

- RackのHTTP/2サポートについて
  - https://techracho.bpsinc.jp/hachi8833/2017_07_03/42195
  - https://github.com/tenderlove/the_metal/issues/5
  - Rackは今回学んだ通り、リクエストに対して1つのレスポンスを返します。しかしHTTP/2だとレスポンスは複数返さないといけないので、さてどうしたものか、という点について言及されています。