Ruby on Rails

【Rails】Axiosでリクエストを送ったらas HTMLとして処理される件【コードリーティング】

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は何も考えないでもきちんと動きますが、ブラックボックス化されている部分が少し気持ち悪いので少しずつ理解していきたいと思いました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!