ログやアクセス制御でAPIかHTMLリクエストかを簡単に区別できるようにする為。
また、ブラウザで.jsonのURLにアクセスしたらエラーにしたい。簡単に見れないようにしたい。
デフォルトの挙動と課題
request.format.json?がtrueになるのは、URLの拡張子が.jsonか、acceptヘッダにapplication/jsonが含まれる(htmlや*がない)場合なので、拡張子なしでもacceptヘッダにあればJSONが返却される。
また、renderで、request.format.json?がtrueの場合にjbuilderが使われ、request.format.html?がtrueの場合はerbのviewが使われるので、一致するviewがないとエラーになる。
URLの拡張子 | Acceptヘッダ | request.format.json? | request.format.html? | レスポンス |
---|---|---|---|---|
.json | JSONが含まれる(htmlや*がない) | true | false | JSON返却 |
上記以外 | true | false | JSON返却 → 406にしたい | |
なし | JSONが含まれる(htmlや*がない) | true | false | JSON返却 → 406にしたい |
上記以外 | false | true | HTML返却 |
参考: request.format.json?がtrueになる条件は?
既存のControllerでDevise Token Auth認証してJSONを返す
defaults formatの挙動
routesでdefaults formatを指定している場合は、URLの拡張子がなくてもJSON扱いになる。
また、下記のようにRSpecとかでformat指定しても、リクエストURLに.jsonは入らない。
subject { get xxx_path(format: :json) }
下記でも同じ.jsonが入らないURLになる。
subject { get xxx_path(format: nil) }
方針
defaults formatを使わずに、明示的に406(not_acceptable)を返すようにする。
renderで対象のviewがない場合に返すHTTPステータスと同じ。
config/routes.rb
draw :infomations
config/routes/infomations.rb
- defaults format: :json do - get 'infomations/important', to: 'infomations#important', as: 'important_infomations' - end + get 'infomations/important', to: 'infomations#important', as: 'important_infomations'
JSONとHTMLの両方を返却する場合
ApplicationAuthControllerを継承するようにしているので、今回はここに追加。
app/controllers/application_auth_controller.rb
class ApplicationAuthController < ApplicationController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token, unless: :format_html? + prepend_before_action :not_acceptable_response private + # リクエストに不整合がある場合、HTTPステータス406を返却 + def not_acceptable_response + if format_html? + head :not_acceptable unless accept_header_html? + else + head :not_acceptable unless accept_header_api? + end + end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base private # URLの拡張子がないかを返却 def format_html? request.format.html? end # acceptヘッダにJSONが含まれる(ワイルドカード不可)かを返却 def accept_header_api? !(%r{,application/json[,;]} =~ ",#{request.headers[:ACCEPT]},").nil? end # acceptヘッダが空か、HTMLが含まれる(ワイルドカード可)かを返却 def accept_header_html? request.headers[:ACCEPT].blank? || !(%r{,text/html[,;]} =~ ",#{request.headers[:ACCEPT]},").nil? || !(%r{,\*/\*[,;]} =~ ",#{request.headers[:ACCEPT]},").nil? end
JSONまたはHTMLのみ返却する場合
app/controllers/application_controller.rb
# APIリクエストに不整合がある場合、HTTPステータス406を返却(明示的にAPIのみ対応にする場合に使用) def not_acceptable_response_not_api_accept head :not_acceptable if format_html? || !accept_header_api? end # HTMLリクエストに不整合がある場合、HTTPステータス406を返却(明示的にHTMLのみ対応にする場合に使用) def not_acceptable_response_not_html_accept head :not_acceptable if !format_html? || !accept_header_html? end
app/controllers/infomations_controller.rb
class InfomationsController < ApplicationAuthController + prepend_before_action :not_acceptable_response_not_api_accept, only: %i[important]
HTMLのみ返却する場合は、not_acceptable_response_not_html_acceptにすればOK
今回のコミット内容
※RSpecやDevise Token Auth使っている部分も同様の修正を入れています。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/6a0fb22e942bc7e24a4421a9f605929a3612b311