finatraのhttpサーバーを作成している時にフロントエンドサーバーからCORSで通信拒否されたので、Access-Control-Allow-Originパラメータを全レスポンスに付与した時のメモです
※Access-Control-Allow-Origin付与はnginxを経由させてheader付与するようにすればfinatraのhttpサーバー側で実装しなくても解決します
起こった問題
大きな問題としては、全てのレスポンスにヘッダーを付与したい
ということで、その中で対応しないといけないのが
- バリデーションでエラーになったリクエストにも指定したヘッダーを付与したい
- HTTPメソッドがGETのエンドポイントに対してのOPTIONSリクエストを送れるようにしたい
2は以下と同じような問題でした
Finatra access-control-allow-origin
問題は上記の2つでした
1つ目の問題は、finatraのバリデーションがルーティングの前に行われるのでcontroller内でresponseにヘッダーを付与しようとしても、そのコードにたどり着くこと無くレスポンスしてしまうのでヘッダーが付与できませんでした
これはfilterを使用することによって解決できます
2つ目の問題はCORSのpreflightリクエストによる問題で、そのエンドポイントが存在しているか確認するためにHTTPメソッドOPTIONSでリクエストをするのですがcontrollerではPOSTを指定しているので見つからずに404を返してしまうという問題でした
実際のシチュエーションとしてAngularから送信されるリクエストを解決するために辿った道のりです
- まず今までnginx経由でfinatra httpサーバーにリクエストしていたのをGCPのLB経由でfinatra httpサーバーを公開したのでnginxでは設定していたCORSヘッダーが無くアクセス拒否されてしまう
- 直接controllerのresponseにヘッダー付与しよう
- 1つ1つresponseにヘッダー付与していくの人類がやるべきではない
- 全レスポンスにヘッダー付与したい Railsのbeforeやafterみたいに全部のリクエストにヘッダー付けるやつ欲しい -> finagleのfilterというのを使えば良いらしい
- Filterを作成して適用してみる POSTは問題なくなった -> 何故かGETリクエストで404が出る
- CORSでリソースを取得するにはpreflightリクエストをOPTIONSで送信するらしい なるほどgetリクエストを指定しているエンドポイントが見つかるはずが無い しかもpreflightリクエストを送信するか決めるにはリクエストヘッダーの条件にもよるらしい
- OPTIONSリクエストは全て200を返すようにしたい
- filterにOPTIONSでリクエストが来た時は200を返すコードを追記しよう
- 解決
Railsで言うところのアクションのbeforeとafterみたいなのをfinagleのfilter使って解決したらCORSのpreflightリクエストというのにも対応しなければいけなかった感じです
手順
headerを付与する手順です
- com.twitter.finagle.SimpleFilterを継承したフィルタークラスを作成 Injectable
- HttpRouterのインスタンスに適用したいfilterをセット
これだけです
実装
まずはフィルタークラスです
finatraのvalidationはルーティングの前に適用されるので全てのリクエストに対するレスポンスにヘッダーを付与するにはそれより前にfilterを通す必要があります
今回問題になったのはCORSなので、preflightリクエスト用にOPTIONSメソッドにもレスポンスのステータスコードを設定しないと404が出続けてAPIに全然アクセスできないという自体になってしまいます
ので、HTTPメソッドがOPTIONSだった場合は無条件で200を返すようにしています
package aaaa.bbbb.filter
import javax.inject.{Inject, Singleton}
import com.twitter.finagle.{Service, SimpleFilter}
import com.twitter.finagle.http.Status.{Ok => StatusOk}
import com.twitter.util.Future
@Singleton
class RequestBeforeFilter[Req <: Request, Rep <: Response] @Inject()
extends SimpleFilter[Request, Response] {
def apply(request: Request, service: Service[Request, Response]): Future[Response] = {
val res = service(request)
if (request.method.name == "OPTIONS") res.map { // OPTIONSであれば200
_.statusCode(StatusOk.code)
}
res.map { // responseにAccess-Control-Allow-*各種ヘッダ追加 全てのresponseにこのヘッダが付与される
_.headerMap
.set("Access-Control-Allow-Credentials", "true")
.set("Access-Control-Allow-Headers", "Origin, Authorization, Accept")
.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT")
.set("Access-Control-Allow-Origin", "*")
}
res
}
}
次はHttpRouterのインスタンスに適用したいfilterをセットします
コメントにも書いてますが
(true)はroutingの前にfilterを通すようにするbeforeRoutingにtrueを渡しています
beforeRouting = trueを設定しているfilterはafter(CommonFilters)のようなfilterよりも先に適用させないといけないぽいです
class MainServer extends HttpServer with Logging {
override val modules = Seq(RealCardSaveModule)
override def defaultFinatraHttpPort: String = ":9000"
override def defaultHttpServerName: String = "0.0.0.0"
override def configureHttp(router: HttpRouter): Unit = {
router
.filter[RequestBeforeFilter[Request, Response]](true) // 上で作成したfilterを適用させる (true)はroutingの前にfilterを通すようにする after(CommonFilters)のようなfilterよりも先に適用させないといけないぽい
.filter[CommonFilters]
.add[ControllerA]
.add[ControllerB]
.add[ControllerC]
.add[ControllerD]
}
}
これでGET, POST, PUT, DELETE, OPTIONSリクエストでもバリデーションエラー時でも指定したヘッダを付与できました
所感
finatraは英語情報とソースコードを読ませるのが好き
ググって見つけた参考ページが沢山あったのですがMacが落ちてしかもシークレットウィンドウだったので全部消えました、南無
それでは以上です。