committee-railsを使ったら”Content-Type” request header must be set to “application/json”.というエラーが出た

committee-railsとRSpecを連携させてOpenAPIの定義と実装が一致しているかテストしてたんですが、その際に"Content-Type" request header must be set to "application/json".というエラーが出てしまい、解決に時間がかかりました。

備忘録としてまとめます。

バージョン

  • Ruby: 3.2.2
  • Rails: 7.1.3.2
  • rspec-rails: 6.1.2
  • committee-rails: 0.8.0

記事の信頼性

  • ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
  • 実務ではVue.jsとRailsを毎日書いています。
  • 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
目次

問題

学習のためスキーマ駆動開発の流れでTODOアプリを作っていました。

現在のOpenAPI定義は次のとおりです。

openapi: 3.0.0
info:
  title: APIv1
  version: 1.0.0
  description: APIv1 for todo-app rails
servers:
  - url: http://127.0.0.1:3000/

paths:
  /api/v1/todos:
    get:
      tags: ["todo"]
      operationId: apiV1GetTodos
      summary: 全てのTodoを取得する
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  todos:
                    type: array
                    items:
                      $ref: "#/components/schemas/Todo"
    post:
      tags: ["todo"]
      operationId: apiV1PostTodos
      summary: Todoを作成する
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: "積読を減らす"
                description:
                  type: string
                  example: "技術書を1冊読み終える"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Todo"
        "400":
          description: Bad Request
        "403":
          description: Forbidden
        "422":
          description: Unprocessable Entity
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: array
                    items:
                      type: string
components:
  schemas:
    Todo:
      properties:
        id:
          type: integer
          format: int64
          example: 1
        title:
          type: string
          example: "積読を減らす"
        description:
          type: string
          example: "技術書を1冊読み終える"
        done:
          type: boolean
          default: false
      required:
        - id
        - title
        - done

この定義を元にした実装は次のとおりです。

class Api::V1::TodosController < ApplicationController
  def index
    @todos = Todo.all.order(:id)
    render json: { todos: @todos }
  end

  def create
    @todo = Todo.new(todo_params)
    if @todo.save
      render json: { todo: @todo }
    else
      render json: { error: @todo.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def todo_params
    params.require(:todo).permit(:title, :description)
  end
end

最後にこの2つのAPIに対するテストコードもRSPecで書きました。

require 'rails_helper'

RSpec.describe 'Api::V1::Todos', type: :request do
  describe 'GET /index' do
    it 'HTTPステータスが成功になること' do
      get '/api/v1/todos'
      expect(response).to have_http_status(:success)
    end

    it 'Todoのリストを返すこと' do
      todo = Todo.create(title: 'Test Todo', done: false)
      get '/api/v1/todos'
      expect(response.body).to include(todo.title)
    end

    it 'Todoのリストを正しい順番で返すこと' do
      todo1 = Todo.create(title: 'Test Todo 1', done: false)
      todo2 = Todo.create(title: 'Test Todo 2', done: false)
      get '/api/v1/todos'
      expect(response.body.index(todo1.title)).to be < response.body.index(todo2.title)
    end

    it 'リクエストスキーマに準拠していること' do
      get '/api/v1/todos'
      assert_request_schema_confirm
    end

    it 'レスポンスコード200でレスポンススキーマに準拠していること' do
      get '/api/v1/todos'
      assert_response_schema_confirm(200)
    end
  end

  describe 'POST /create' do
    it 'HTTPステータスが成功になること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
      expect(response).to have_http_status(:success)
    end

    it '新しいTodoを作成すること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
      expect(Todo.last.title).to eq('Test Todo')
    end

    it 'リクエストスキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
      assert_request_schema_confirm
    end

    it 'レスポンスコード200でレスポンススキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
      assert_response_schema_confirm(200)
    end

    it 'レスポンスコード422でレスポンススキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: '' } }
      assert_response_schema_confirm(422)
    end
  end
end

しかしテストを実行すると次のようにCommittee::InvalidRequestとエラーが出てしまいます。

todo-app % docker compose run --rm rails rspec spec/requests/api/v1/todos_spec.rb 
[+] Running 1/0
 ✔ Container todo-app-db-1  Running                                                                                                                                0.0s 

Api::V1::Todos
  GET /index
    returns http success
    returns a list of todos
    returns a list of todos in the correct order
    conforms to request schema
    conforms to response schema with 200 response code
  POST /create
    returns http success
    creates a new todo
    conforms to request schema (FAILED - 1)
    conforms to response schema with 200 response code
    conforms to response schema with 422 response code

Failures:

  1) Api::V1::Todos POST /create conforms to request schema
     Failure/Error: assert_request_schema_confirm
     
     Committee::InvalidRequest:
       "Content-Type" request header must be set to "application/json".
     # /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3/request_validator.rb:35:in `check_content_type'
     # /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3/request_validator.rb:16:in `call'
     # /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3.rb:61:in `request_schema_validation'
     # /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3.rb:18:in `request_validate'
     # /usr/local/bundle/gems/committee-5.1.0/lib/committee/test/methods.rb:17:in `assert_request_schema_confirm'
     # ./spec/requests/api/v1/todos_spec.rb:51:in `block (3 levels) in <top (required)>'

Finished in 0.21618 seconds (files took 2.1 seconds to load)
10 examples, 1 failure

Failed examples:

rspec ./spec/requests/api/v1/todos_spec.rb:49 # Api::V1::Todos POST /create conforms to request schema

todo-app % 

openapi.yamlにはapplication/jsonを指定しているのになぜだ?となり、理由がわからずハマってしまいました、、。

なおRSpecとcommittee-railsの連携はspec/rails_helper.rbにて次のように記述しています。

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'

abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'

begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  abort e.to_s.strip
end
RSpec.configure do |config|
  config.fixture_paths = [
    Rails.root.join('spec/fixtures')
  ]

  config.use_transactional_fixtures = true

  config.infer_spec_type_from_file_location!

  config.filter_rails_from_backtrace!

  # モジュール名 (FactoryBot)を省略するために記述
  config.include FactoryBot::Syntax::Methods

  # OpenAPIのスキーマチェックを行う
  config.include Committee::Rails::Test::Methods
  config.add_setting :committee_options
  config.committee_options = {
    schema_path: Rails.root.join('docs/openapi/openapi.yaml'),
    # committee 5 から明示的な指定が推奨された
    strict_reference_validation: true
  }
end

解決方法

結論として、次のようにRSpecのテストコードを修正することでテストが成功するようになりました。

require 'rails_helper'

RSpec.describe 'Api::V1::Todos', type: :request do
  let(:request_header) { { 'Content-Type' => 'application/json' } }

  describe 'GET /index' do
    it 'HTTPステータスが成功になること' do
      get '/api/v1/todos'
      expect(response).to have_http_status(:success)
    end

    it 'Todoのリストを返すこと' do
      todo = Todo.create(title: 'Test Todo', done: false)
      get '/api/v1/todos'
      expect(response.body).to include(todo.title)
    end
    
    it 'Todoのリストを正しい順番で返すこと' do
      todo1 = Todo.create(title: 'Test Todo 1', done: false)
      todo2 = Todo.create(title: 'Test Todo 2', done: false)
      get '/api/v1/todos'
      expect(response.body.index(todo1.title)).to be < response.body.index(todo2.title)
    end
    
    it 'リクエストスキーマに準拠していること' do
      get '/api/v1/todos'
      assert_request_schema_confirm
    end
    
    it 'レスポンスコード200でレスポンススキーマに準拠していること' do
      get '/api/v1/todos'
      assert_response_schema_confirm(200)
    end
  end

  describe 'POST /create' do
    it 'HTTPステータスが成功になること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
      expect(response).to have_http_status(:success)
    end

    it '新しいTodoを作成すること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
      expect(Todo.last.title).to eq('Test Todo')
    end
    
    it 'リクエストスキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
      assert_request_schema_confirm
    end
    
    it 'レスポンスコード200でレスポンススキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
      assert_response_schema_confirm(200)
    end
    
    it 'レスポンスコード422でレスポンススキーマに準拠していること' do
      post '/api/v1/todos', params: { todo: { title: '' } }.to_json, headers: request_header
      assert_response_schema_confirm(422)
    end
  end
end

追加したのは以下の2点です。

  • paramsto_jsonでJSON形式に変換
  • headersオプションでリクエストヘッダーを指定

OpenAPI定義でapplication/jsonと定義しているため、paramsをJSON形式へ変換した上で、リクエストヘッダーでもapplication/jsonを指定する必要がありました。

リクエストボディにデータを含めてパラメータをサーバへ渡したい場合は、このように書かないといけないようです。

おわりに

てっきりOpenAPI定義の書き方やcommittee-railsとRSpecの設定の問題だと思っていたんですが、RSpecの書き方が問題だったんですね。

Railsエンジニアにおすすめの記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

未経験でSESから従業員300名以上の自社開発企業に転職しました。業務や個人開発で直面した問題や、転職・学習の経験を発信していきます。

コメント

コメントする

目次