###0. 導入
Railsでアプリケーションを作成しデプロイしました。しかし、Nginxの設定が原因でPOST Requestを行ったタイミングでエラーが発生してしまいました。どのようにエラーを解消したのかということをまとめます。
##1. 概要
###1.1. 構成
SakuraのVPSを借りてCentOS7系をインストールしています。
NginxをReverse Proxyとして動作させ、HTTPS通信を終端化し、リクエストをアップストリームサーバ(puma)に転送します。
pumaとRailsアプリはRack1を介してrequestとresponseをやりとりします。
###1.2. 問題点
少し長目の記事なので最初にどこが問題であったのか書いておきます。
今回はNginxをReverseProxyとした際に、アップストリームサーバに必要なHTTPヘッダーを渡せていなかったことが問題でした。
##3. トラブルシュート
###3.1. Status Code 422 とは?
MDN web docsを参照してみます。
422 Unprocessable Entity:
The HyperText Transfer Protocol (HTTP) の 422 Unprocessable Entity 応答状態コードは、サーバーが要求本文のコンテンツ型を理解でき、要求本文の構文が正しいものの、中に含まれている指示が処理できなかったことを表します。
わからない。「中に含まれている指示が処理できなかった」という表現がいささか抽象的です。
If you are the application owner check the logs for more information.
「アプリのオーナーなら詳細についてログを見てください」とのことなのでVPSにログインしRailsのログを確認してみます。
###3.2. Logの確認
VPSにログインし下記のログを確認します。
(省略)
W, [*] WARN -- : [*] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com)
I, [*] INFO -- : [*] Completed 422 Unprocessable Entity in 2ms
F, [*] FATAL -- : [*]
F, [*] FATAL -- : [*] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
F, [*] FATAL -- : [*]
F, [*] FATAL -- : [*] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request'
(省略)
最初のWARNと最後のFATALに、具体的な情報がありそうに思えます。
W, [〜] WARN -- : [〜] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com)
F, [〜] FATAL -- : [〜] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request'
WARNでは「HTTPのオリジンヘッダーがrequest.base_url
にマッチしていない。」と言われています。FATALでは request_forgery_protection.rb
がエラーを出しています。まずはエラーを出しているメソッドを確認してゆきます。
###3.3. request_forgery_protection.rbを見てみる
request_forgery_protection.rb
を一部添付します。
require "rack/session/abstract/id"
require "action_controller/metal/exceptions"
require "active_support/security_utils"
require "active_support/core_ext/string/strip"
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
end
module RequestForgeryProtection
extend ActiveSupport::Concern
(省略)
def verify_authenticity_token # :doc:
mark_for_same_origin_verification!
if !verified_request?
if logger && log_warning_on_csrf_failure
if valid_request_origin?
logger.warn "Can't verify CSRF token authenticity."
else
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
end
end
handle_unverified_request
end
end
(省略)
def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
request.origin.nil? || request.origin == request.base_url
else
true
end
end
(省略)
end
end
valid_request_origin?
メソッドでrequest.origin == request.base_url
を比較しています。この比較がfalse
を返すとverify_authenticity_token
メソッドが上述のWARNメッセージを出すようです。
base_url
メソッドは、lib/action_dispatch/http/request.rb
がinclude
しているRack::Request::Helpers
に定義されています。
###3.3. Rackを見てみる
request.rb
を一部添付します。
require 'rack/utils'
require 'rack/media_type'
module Rack
class Request
class << self
attr_accessor :ip_filter
end
(省略)
module Env
(省略)
def get_header(name)
@env[name]
end
(省略)
end
module Helpers
(省略)
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
(省略)
def scheme
if get_header(HTTPS) == 'on'
'https'
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
'https'
elsif forwarded_scheme
forwarded_scheme
else
get_header(RACK_URL_SCHEME)
end
end
(省略)
def base_url
url = "#{scheme}://#{host}"
url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
url
end
(省略)
def forwarded_scheme
scheme_headers = [
get_header(HTTP_X_FORWARDED_SCHEME),
get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0]
]
scheme_headers.each do |header|
return header if ALLOWED_SCHEMES.include?(header)
end
nil
end
end
include Env
include Helpers
end
end
base_url
メソッドを見つけました。
このメソッドの中でscheme
メソッドが呼び出されています。get_header
はenv
2という変数が保持しているハッシュから、引数に指定されたKey
が持つValue
を取得します。 get_header
の引数となるKey
にはHTTPヘッダ3が指定されているようなので、Nginxの設定でヘッダを付与してみることにします。
###3.4. Nginxの設定を直す
upstream app-name {
server unix:/var/www/app-name/shared/tmp/sockets/devcamp-portfolio-puma.sock fail_timeout=0;
}
server {
if ($host = self-ref-penguin.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name self-ref-penguin.com;
root /var/www/app-name/current/public;
}
server {
listen 443 ssl http2 default_server;
# listen [::]:443 ssl http2 default_server;
server_name self-ref-penguin.com;
ssl_certificate ***;
ssl_certificate_key ***;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers ***;
ssl_protocols ***;
location ~ ^/assets/ {
root /var/www/app-name/current/public;
}
try_files $uri/index.html $uri @app-name;
location / {
proxy_pass http://app-name;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
# この設定が抜けていました
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
(省略)
}
X-Forwarded-Protoヘッダーの設定を追記し、「ユーザのリクエストが使用したHTTPスキームを指定する」ように修正。systemctl restart nginx.service
を実行します。再度ページにアクセスしログインを実行。今度はログイン(POST)が成功したことを確認できました。
今回の構成ではNginxがReverse ProxyとなりHTTPSのアクセスを終端化していたのでした。今回のエラーの原因は下記の説明に要約されるかと思います。
アップストリームに伝える必要がある情報にクライアントのリクエスト情報があります。アップストリームサーバーへのリクエストはすべてプロキシを経由するため、そのままではクライアントの送信元アドレスや使用したプロトコルがわからなくなってしまいます。このため、クライアントのリクエスト情報をいくつかのヘッダを付与することでアップストリームへ伝えることができます。これらのヘッダは標準化されていませんが、Squid、Apache HTTPサーバなどでデファクトスタンダードとして使用されており、RubyのRackインタフェースもこれらのヘッダを解釈します。4
##4. まとめ
エラーの原因についてはシステムの構成をしっかりと理解していれば、もっと簡単にあたりをつけることが可能であったと思います。
-
Rackについては以下2つの記事によくまとまっています
Rails on Rack
What is Rack in Ruby?
##2. エラー発生
###2.1. 422エラーが返ってきた
ブログを作成し、デプロイも完了。早速ログインしブログを投稿しよう、と思った矢先…
なんかエラーが出てる…Status Code 422って何だ? ↩ -
EnvについてはStack over flowの質問を参照しました。「env is just a hash. Rack itself and various middlewares add values into it.」「envはハッシュで、Rackや様々なミドルウェアがこれにValueを加えていきます」 ↩
-
HTTPヘッダーの種類についてはMDN web docsを参照し、適切な値を探しました。 ↩