トップ 追記

2019-06-15 [長年日記]

_ 今さら改めてRack middleware

Understanding Rack Middleware

がとても分かりやすかったのでこれをもとにしたいと思う。

Rackの基本構造

  • Rack middleware はたまねぎ構造で rack app が他の rack app をくるんだ形
  • 受け取った request を外側の app が内側の app へバケツリレーし、response を内側の app から外側の app へリレーする
  • この組み立てのために Rack::Builder の use と run を使う

Rackアプリの基本形

  • callメソッドを持っていること

だけ。

call メソッドを持っていれば class だろうが Proc オブジェクトだろうが Method オブジェクトだろうがなんでもよい。

まぁ本当は call メソッドの引数の型と戻り値の型がどうってのはあって、Hash を受け取って Rack::Response を finish して返しなさいってことなんだけど、それは置いておく。

Rack middlewareは二種類ある

  1. バケツリレーする middleware
  2. response を直接返す middleware

どちらも Lint 的には valid なので少しややこしいのと、直接 response を返してるのは middleware と呼ぶのか?という気はするけど、まぁそういうものらしい。

前者のバケツリレーを実現するたまねぎ構造のために

rack app のところでは call があればよいという話だったんだけど、たまねぎ構造のためには app を受け取ってリレーする必要がある。これをどう実現するのかというと、ちょうどいいサンプルが rack 2.0.7 の

Rack::Builder#use

のコメントに例が書かれているので貼っておく。

#   class Middleware
#     def initialize(app)
#       @app = app
#     end
#
#     def call(env)
#       env["rack.some_header"] = "setting an example"
#       @app.call(env)
#     end
#   end
  • initialize で app を受け取ってどこかのインスタンス変数に保存しておく
  • call で env を受け取って、call の中でインスタンス変数から取り出してそいつの call を呼ぶ

という形になっている。どこでこれが決まっているかというと、use の中のこれ。

@use << proc { |app| middleware.new(app, *args, &block) }

つまり、

use Middleware, *args, &block

と書くと

Middleware.new(app, *args, &block)

で他の app をくるむので、initialize で受け取る処理を書きましょう、となる。

直接返す

直接返すっていうか、直接ではないんだけど、要はこの @app.call(env) の実行がなければ内側の middleware, app は呼び出されないので、外側の middleware だけで処理が完結するという形。

例えば Rack::Attack という middleware があるんだけど、この 6.0.0 の call は以下のようになっている。

 def call(env)
   env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
   request = Rack::Attack::Request.new(env)

   if safelisted?(request)
     @app.call(env)
   elsif blocklisted?(request)
     self.class.blocklisted_response.call(env)
   elsif throttled?(request)
     self.class.throttled_response.call(env)
   else
     tracked?(request)
     @app.call(env)
   end
 end

アクセスを block するのが目的なので、 blocklist に引っかかった場合は中の app は呼ばれず、自分で用意している response を返す形になっている。

順番をあとから制御したい場合はどうしたらいいの?

基本的に rack middleware は use で並べた順番にだんだんと内側に middleware をくるんでいくので、この順番で実行される。

しかし例えば Rails では

middleware.insert 0, Middleware

という書き方で順番を後から制御できる。しかしこの記法、これは Rack の機能じゃないのです。Rails::Configuration::MiddlewareStackProxy ってやつがいい具合にやってくれている。

逆にこの機能に依存せずに request を受け取ってすぐに受け取る何かを作ろうと思ったらどうするか?

そう、Rack は app を middleware でくるめばいいだけなので、config.rb で

run Middleware.new(Rails::Application)

みたいなことをやればよい。なるほどなぁ。

middlewareの中身はこんな感じ

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

  def call(env)
    status, headers, body = @app.call(env)
    res = Rack::Response.new(body, status, headers)

    res.xxxxx  # なんかやる

    res.finish
  end
end

参考


2019-06-05 [長年日記]

_ Hashie::Dashでお手軽validationあるいはリソースを信用できるかどうか問題

intridea/hashie: Hashie is a collection of classes and mixins that make hashes more powerful.

まとめ

  • Ruby で nil を避ける方法は事前に validate する方法と取得時、利用時に気をつける方法がある
  • 事前に validate できる方が安心だしテストも楽(影響範囲が小さい)
  • いくつか validator はあるが Hashie::Dash は validate 機能内蔵していてかなりお手軽でいい感じ
    • 独立した validator を使うのは大袈裟だなと感じるようなカジュアルなシーンにハマりそう

Rubyでnilを避ける方法百選

nil が入るかもしれないところのコードをどうするかにはいくつか方法がある。

  • ActiveSupport の try
  • ぼっち演算子 (&. )
  • 文字列と分かっているなら .to_s
  • 逆に Hash から取り出す時に死んでほしい Hash#fetch
  • 素朴に nil?

などなど。

逆に DBMS 前提なら例えば ActiveRecord で

create_table do |t|
  t.string  :name, null: false
end

とすると name に nil を入れて save することはできない。

ま、扱うオブジェクトによっていろいろある。

事前に弾くのか、取得時に考慮するのか

上に挙げた例はアプローチが正反対である。

  • 前者は値の取得時、利用する際に nil を避ける方法
  • 後者はそもそも nil が入らないようにする方法

今回は後者の nil が入らないようにするということを validation と呼ぶことにしている。

Hashie::Dashの使い方

Hashie::Dash はみんな大好き Hashie::Mash に事前の property 定義が書けるもの。以下のような感じ。

class Model < Hashie::Dash
  property :foo, required: true
end

これで

Model.new(foo: nil)

とすると

ArgumentError: The property 'foo' is required for Model.

というエラーになる。おぉ、これならふいに nil を踏むことはなくなるじゃん。逆に

Model.new(foo: 'nil', bar: 1)

になると

NoMethodError: The property 'bar' is not defined for Model.

で、余計な property を突っ込むこともできない。

m = Model.new(foo: 'nil')
m.bar = 1

だと

NoMethodError: undefined method `bar=' for #<Model foo="nil">

おおおお。初期化時だろうと動的な変更だろうと弾いてくれる。

ということで Dash は以下のようなものらしい。

  • Hashie::Dash は事前に property を定義することができる Mash のようなもの
  • property の validation が自動で行われる
  • required: true の property は必須でかつ nil を与えることはできない
  • 余計な property を追加することもできない

intridea/hashie: Hashie is a collection of classes and mixins that make hashes more powerful.

の例を見ると他の property を参照することもできる。けっこう便利。

まぁ Rails 使ってるなら ActiveModel できっちり固めるという方法もあると思うし、Hanami みたいに dry-validation で schema を書くという方法もあると思うけど、もっとカジュアルに使いたいよねって時に Hashie::Dash で validate しておくと、単なる Hash より

  • いい具合にオブジェクトっぽく振舞ってくれる
  • 事前の定義で比較的安全に扱えるようにできる

ということが分かった。

なるほどな。

Tags: Ruby

2019-06-01 [長年日記]

_ Rails + rack-corsの設定をRSpecだけである程度お手軽にテストする

やりたいこと

  • CORSの設定ミスやエラーをできるだけ早期に発見する
  • CORSの設定のテストをできるだけブラウザを使った手動テスト以外の方法で実現する
  • 以上をできるだけ手軽に実現する

CORSのテストはとてもやっかい

CORS は非常にやっかいで、そもそも用語が request 側と response 側で分かりにくいというのもあるんだけど、何よりテストが面倒くさい。JavaScript からの CORS リクエストの動作を完全にテストしようとすると、例えば Rack アプリの場合、

  • cross-origin リクエストを生成するために JavaScript 向けに HTTP サーバを立てる
  • rack サーバを立てる
  • JavaScript のテストコードを書く
  • 以上を連動させる自動テストを作る

ということになってしまう。これはかなり面倒くさい。もちろん rack サーバ側は Rails アプリをそのまま使うとなると E2E になる。

rspec-railsだけでどこまでいけるのか?

ということで、どこまでだったら RSpec だけで頑張れるかをちょっと試してみた。環境は以下の通り。

  • rails 5.2.3
  • rspec-rails 3.8.2
  • rack-cors 1.0.3
rack-corsのdebugを有効に
config.middleware.insert_before 0, Rack::Cors do

で書いていく部分に debug 設定を追加。こんな感じ。

config.middleware.insert_before 0, Rack::Cors, debug: !Rails.env.production?

こうしておくと production の時以外は X-Rack-CORS* なヘッダが追加されるので、test 時にも利用できる。

ここで大事なのは

X-Rack-CORS: miss

というヘッダが現れるかどうか。これが現れたら少なくともサーバ側の動作としては CORS の要件を満たさないので目的の response を返してくれることはない。

request specを書く

これには以下のような理由がある。

  • Rails アプリが E2E で動作するように
    • rack-cors は rack レイヤーであって Rails の Controller, Routing などのテストでは不十分
  • ある程度自由に HTTP を喋れるように
    • Capybara のボキャブラリに縛られると自由が効かない

注意点。OPTIONS を直接呼べない。

RSpec::Rails::RequestExampleGroup の利用している ActionDispatch::Itegration::Runner で define_method で決まっているっぽい。

     %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
       define_method(method) do |*args|

こゆことです。つまり Preflight リクエストを直接テストできないことを意味している。ということで書けるのは Origin ヘッダ付きの GET とか POST とかそういうものになる。

get '/api/v1/posts.json', headers: { 'Origin' => 'http://example.net' }

みたいな感じ。例えば production で動く Origin をここで指定してあげると deploy する前にテストすることができる。

※ Rails 5.1 でオプションの与え方が変わっているらしいので注意が必要。ここは RSpec ではなく ActionDispatch の方に引っぱられているので、Rails のバージョンによって変わってしまう。

cf. Request tests fail after upgrading to Rails 5

この時、

response.headers

の中身は以下のようになる。

{ "Content-Type" => "application/json; charset=utf-8",
  "Vary"         => "Origin",
  "X-Rack-CORS"  => "miss; no-origin",
..

この "X-Rack-CORS" => "miss" がポイント。これがあったら Rack のレベルで弾かれている。つまり Controller に処理が渡ることはない。

理由については rack-cors の中で以下のように定義されている。

module Rack
  class Cors

    protected
     class Result
       HEADER_KEY = 'X-Rack-CORS'.freeze

       MISS_NO_ORIGIN = 'no-origin'.freeze
       MISS_NO_PATH   = 'no-path'.freeze

       MISS_NO_METHOD   = 'no-method'.freeze
       MISS_DENY_METHOD = 'deny-method'.freeze
       MISS_DENY_HEADER = 'deny-header'.freeze
stub outなどは適宜ご自由に

E2E なので Model 側の準備も必要になってしまうが、CORS の部分だけをチェックしたいのであれば適当に Stub Out してあげればなんとかなる。これはいつもの通り。

RR.stub(Post).recents { [] }

みたいなのでたぶん十分。

まとめ - できること、できないこと

  • 少なくとも Origin をまたいだ HTTP リクエストのフリはできる
    • XHR のフリもできる
  • Origin が食い違っていることで動かない様子や Origin をオウム返しして動く様子など Rack サーバ側のテストはできる
  • JavaScript が動いているわけではないので、XHR の mode 設定はできない
    • つまり withCredentials 周りで引っかかるか否かはテストできない

欲を言えば XHR のモード設定とかしたいけれども、少なくとも今回の方法で 単純な HTTP のレベルと XHR や Fetch などの API レベルの切り分けはできる ので、そういう使い方になるのかなと思っている。

おまけ

今回 RSpec でどこまでやれるのかを確かめるきっかけになったのは、実は rack-cors が 1.0 で default の動作が変わったこと、および XHR に利用していたライブラリがなぜか withCredentials = true で動いていたことのコンボで CORS で取得していたコンテンツが表示できなくなっていたという問題だった。こうした問題に対してあれこれ試して分かったのは

  • CORS で意図通りにコンテンツを扱えないという問題を取得できる堅牢な API はない
    • XHR および Fetch API では通信の失敗とコンテンツが利用できないエラーは区別できない
  • ブラウザのconsole上には何か出ている
  • つまり production のモニタリングで失敗を検知するのは難しい

ということだった。*1 少なくとも本気でやるなら最初に書いたようなテストの自動化を用意してあげる必要はありそうなんだけど、ちょっと今の環境でそこまでやる手間を掛けるのはアレだなと思って別な方法を探していた次第。

はー、思ったより疲れたよ。CORS ってほんとに狭間にあるので、意外と知見がまとまってないんだよなー。

参考

Tags: Ruby

*1 もしかしたらイマドキの賢い Fetch クライアント的なものではうまく扱える可能性もないではないけど。


2019-05-28 [長年日記]

_ VueとTime Travel Debuggingについて分かったことと、それでもEventのLogが欲しい気がする話

Time Travel Debuggingってなんだっけ

Vuexを用いた開発プロジェクト用にガイドラインを作成した話 - Qiita

を読んで、

最終的にチーム内で合意に至ったのは「パフォーマンスが懸念されるなどの特別な理由がない限り、原則として全ての状態をVuexのStoreで管理する」という方針です。

この決定に至った理由としては以下が挙げられます。

  • Time Travel Debuggingが可能になるため、デバッグがしやすくなる
  • テストがしやすくなる
  • 複数人による開発の中で状態管理に一貫性が生まれる

というくだりを読んで、あれ、Time Travel って Vuex 使わないとできないんだっけ? と思って確認してみた。

試してみた

Vue DevTools 5.0.0 で確認(なぜか 5.1.0 がうまく動かなかった)

Vue DevTools 上の Vuex タブで mutations の履歴を取得できる。この mutation は record された単なる events と違い、実際の変更の記録であり、以下の3つの操作を行える。

  • export
  • import
  • commit all

ということで、「手元で起きたことを他の環境に持って行って」再検証することができる。

これが Time Travel Debug.

あーこれ Event Sourcing の話にも通じるのか。

Eventとの違い

Vue DevTools 上では Event も record できるのだけど、Event は単なる Event であり、record はできるが replay はできない。

これは Event はどこで起きるかもその結果を受けて何を行うかも自由なので replay の難易度が高いということかな? Vuex Store への「変更」に関しては確実な操作(ユーザーの操作ではないけど)の記録であり、Vuex Store 上で replay することは Event をそのまま replay するより難しくない気がする。component が正しく Vuex の state の変化に対して Reactive に動作すればこれで十分である。

雑感

各 component のレベルで data の変更に対して Time Travel できるとより便利っぽい。でもそれができれば先のガイドラインの判断基準がなくなっちゃうのか。

それとは別に Event の export くらいはできるようになってほしいんだけどなぁ…。思わん?