RailsにAxiosでサーバーへPOSTした際にContent-Typeでapplication/jsonを指定していましたが、Rails側ではas HTMLとして認識されていたので、この理由についてソースコードを読んだり、有識者に質問するなどして調べたのでその内容を書いていこうと思います。
フォーマットを決めている部分のソースコードを読んでみる
はじまりですが、下のようにaxiosでリクエストを送った際に、Rails側ではas HTMLとして認識されていて、いったいどのようにフォーマットを判断しているのだろうと疑問に思って調べました。(コードは適当です)
axios
.post('/api/users', {
email: 'hoge@example.com',
password: 'password',
})
.then(() => {
toastr.success('登録しました');
})
.catch(() => {
toastr.warning('エラーが発生しました');
});
class Api::UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
render json: { success: true }, status: :ok
else
render json: { success: false }, status: :unprocessable_entity
end
end
end
まずリクエストのヘッダーを調べると以下のようになっていました。
- Accept: application/json, text/plain, */*
- Content-Type: application/json
axiosはデフォルトではAcceptヘッダーにapplication/json, text/plain, */*をセットします。なお、HTTPの使用上、Acceptヘッダーの順序は優先度とは関係がありません。優先度を付ける場合はqvalueを使って表現します。(例) application/json, text/plain;q=0.9, */*;q=0.8)
https://github.com/axios/axios/blob/master/lib/defaults.js#L121
次に、Railsのソースコードより、フォーマットを決めている部分を読んでみました。
def formats
fetch_header("action_dispatch.request.formats") do |k|
v = if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
elsif extension_format = format_from_path_extension
[extension_format]
elsif xhr?
[Mime[:js]]
else
[Mime[:html]]
end
v = v.select do |format|
format.symbol || format.ref == "*/*"
end
set_header k, v
end
end
as HTMLになるためには来るためには12行目まで来る必要がありそうです。
まず、1つ目の条件で4行目でMime[parameters[:format]]とありますが(parametersはparamsのこと)、今回はクエリパラメータでformatを指定していないのでこの条件には合致しません。
2つ目の条件を飛ばして3つ目の条件ではformat_from_path_extensionとあるようにパスの拡張子を示しています。例えば、パスをhttps://example.com/api/users.jsonのように指定すると、この条件に合致するようになります。しかし、今回は拡張子をつけていないのでこの条件にも合致しません。
4つ目の条件xhr?についてですが、これは以下のように定義されており、HTTP_X_REQUESTED_WITHヘッダーでXMLHttpRequestが指定されているときのみtrueになります。今回はそのようなヘッダーをつけていないのでこの条件にも合致しません。
def xml_http_request?
/XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
end
alias :xhr? :xml_http_request?
最後に2つ目の条件を見てみます。valid_accept_headerとあります。or条件で書かれていて重要なのは3行目です。!accept.match?(BROWSER_LIKE_ACCEPTS))とあります。これがfalseだとvalid_accept_headerはfalseを返し条件に合致しなくなります。
def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
(accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
end
それではBROWSER_LIKE_ACCEPTSを見てみます。
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
これは, */*
または*/*,
を表す正規表現です。Accept ヘッダにこれが含まれているとvalid_accept_headerはfalseを返します。
そもそもなぜこのようなことをしているかというと、大体のブラウザではデフォルトでAcceptヘッダに*/*をつけます。
https://developer.mozilla.org/ja/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
主要なブラウザのデフォルトの Accept ヘッダは invalid として扱うという仕様に Rails ではなっているのでこのメソッドがあるようです。axiosでつける Accept ヘッダにも*/*が含まれているので、valid_accept_headerはfalseを返し、この条件にも合致しません。
長くなりましたが、axios で送るリクエストはこのようにifで分岐したすべての条件に合致しないため、12行目が実行され、as HTMLとなるのでした。
まとめ
Axiosでリクエストを送る際はas JSONとなるように以下のいずれかを行う。
- クエリパラメータでformat=jsonを指定する
- パスの拡張子を.jsomにする
- デフォルトのAccept ヘッダをapplication/jsonで上書きする
個人的にもRailsのソースコードを読むいい機会になりました。Railsは何も考えないでもきちんと動きますが、ブラックボックス化されている部分が少し気持ち悪いので少しずつ理解していきたいと思いました。