{"action":"created","issue":{"url":"https://api.github.com/repos/rails/rails/issues/21399","labels_url":"https://api.github.com/repos/rails/rails/issues/21399/labels{/name}","comments_url":"https://api.github.com/repos/rails/rails/issues/21399/comments","events_url":"https://api.github.com/repos/rails/rails/issues/21399/events","html_url":"https://github.com/rails/rails/issues/21399","id":103476848,"number":21399,"title":"autosave: true + protect_from_forgery with: :null_session causes saving invalid record","user":{"login":"olegantonyan","id":2301579,"avatar_url":"https://avatars.githubusercontent.com/u/2301579?v=3","gravatar_id":"","url":"https://api.github.com/users/olegantonyan","html_url":"https://github.com/olegantonyan","followers_url":"https://api.github.com/users/olegantonyan/followers","following_url":"https://api.github.com/users/olegantonyan/following{/other_user}","gists_url":"https://api.github.com/users/olegantonyan/gists{/gist_id}","starred_url":"https://api.github.com/users/olegantonyan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/olegantonyan/subscriptions","organizations_url":"https://api.github.com/users/olegantonyan/orgs","repos_url":"https://api.github.com/users/olegantonyan/repos","events_url":"https://api.github.com/users/olegantonyan/events{/privacy}","received_events_url":"https://api.github.com/users/olegantonyan/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/rails/rails/labels/stale","name":"stale","color":"444444"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":6,"created_at":"2015-08-27T10:31:06Z","updated_at":"2016-01-25T08:21:11Z","closed_at":null,"body":"Hi! I have a very strange bug.\r\nI'm using gaffe gem (https://github.com/mirego/gaffe) to intercept all errors and display them nicely. It uses special controller for errors. But I think it's an issue with rails rather than gaffe (please, correct me I'm wrong).\r\nSo, I have `User` model and `Profile`:\r\n``` ruby\r\nclass User < ActiveRecord::Base\r\n  # Include default devise modules. Others available are:\r\n  # :confirmable, :lockable, :timeoutable and :omniauthable\r\n  devise :database_authenticatable, :registerable,\r\n         :recoverable, :rememberable, :trackable, :validatable\r\n\r\n  has_one :profile, inverse_of: :user, dependent: :destroy, autosave: true\r\n\r\n  after_create do\r\n    create_profile name: 'hello'\r\n  end\r\nend\r\n```\r\n``` ruby\r\nclass Profile < ActiveRecord::Base\r\n  belongs_to :user, inverse_of: :profile\r\n  validates :name, presence: true, length: {maximum: 5}\r\nend\r\n```\r\nAlso I have api controller to update the profile\r\n``` ruby\r\nclass Api::V2::ProfilesController < ApplicationController\r\n\r\n  def update\r\n    profile = current_user.profile\r\n    profile.name = SecureRandom.hex\r\n    profile.save!\r\n  end\r\nend\r\n```\r\nAnd my `ApplicationController` has `protect_from_forgery with: :null_session`.\r\nWhen I trigger `Api::V2::ProfilesController.update` action it fails with `ActiveRecord::RecordInvalid`, then gaffe catches this error and calls its `Api::ErrorsController`. That's where interesting thing happen. \r\nErrorsController is dead simple:\r\n``` ruby\r\nclass Api::ErrorsController < ActionController::Base\r\n  include Gaffe::Errors\r\n\r\n  #protect_from_forgery with: :null_session\r\n\r\n  layout false\r\n\r\n  def show\r\n    output = { error: \"#{@rescue_response}: #{@exception.to_s}\" }\r\n    output.merge! exception: @exception.inspect, backtrace: @exception.backtrace.first(10) if Rails.env.development? || Rails.env.test?\r\n    render json: output, status: @status_code\r\n  end\r\n\r\nend\r\n```\r\nBut the key point is `protect_from_forgery with: :null_session`. Without it all is good. But, with it profile is saved even when it's invalid!\r\n```\r\nStarted PATCH \"/api/v2/my/profile\" for ::1 at 2015-08-27 12:06:05 +0200\r\n  ActiveRecord::SchemaMigration Load (0.6ms)  SELECT \"schema_migrations\".* FROM \"schema_migrations\"\r\nProcessing by Api::V2::ProfilesController#update as JSON\r\n  Parameters: {\"my\"=>true, \"profile\"=>{}}\r\nCan't verify CSRF token authenticity\r\n  User Load (0.3ms)  SELECT  \"users\".* FROM \"users\" WHERE \"users\".\"email\" = ? LIMIT 1  [[\"email\", \"user@example.com\"]]\r\n   (0.1ms)  begin transaction\r\n  SQL (0.4ms)  UPDATE \"users\" SET \"last_sign_in_at\" = ?, \"current_sign_in_at\" = ?, \"sign_in_count\" = ?, \"updated_at\" = ? WHERE \"users\".\"id\" = ?  [[\"last_sign_in_at\", \"2015-08-27 10:00:42.276091\"], [\"current_sign_in_at\", \"2015-08-27 10:06:05.720058\"], [\"sign_in_count\", 23], [\"updated_at\", \"2015-08-27 10:06:05.721133\"], [\"id\", 1]]\r\n   (18.6ms)  commit transaction\r\n  Profile Load (0.4ms)  SELECT  \"profiles\".* FROM \"profiles\" WHERE \"profiles\".\"user_id\" = ? LIMIT 1  [[\"user_id\", 1]]\r\n   (0.1ms)  begin transaction\r\n   (0.1ms)  rollback transaction\r\nCompleted 422 Unprocessable Entity in 67ms (ActiveRecord: 20.6ms)\r\n\r\nActiveRecord::RecordInvalid (Validation failed: Name is too long (maximum is 5 characters)):\r\n  app/controllers/api/v2/profiles_controller.rb:6:in `update'\r\n\r\n\r\nProcessing by Api::ErrorsController#show as JSON\r\n  Parameters: {\"my\"=>true, \"profile\"=>{}, \"error\"=>{\"profile\"=>{}}}\r\nCan't verify CSRF token authenticity\r\n   (0.1ms)  begin transaction\r\n  SQL (0.4ms)  UPDATE \"profiles\" SET \"name\" = ?, \"updated_at\" = ? WHERE \"profiles\".\"id\" = ?  [[\"name\", \"4298b19af767fc891f39c330f24013de\"], [\"updated_at\", \"2015-08-27 10:06:05.783271\"], [\"id\", 1]]\r\n   (18.3ms)  commit transaction\r\nCompleted 422 Unprocessable Entity in 27ms (Views: 0.9ms | ActiveRecord: 18.7ms)\r\n```\r\nNotice UPDATE \"profiles\" at the end.\r\nWithout `protect_from_forgery with: :null_session` in ErrorsController all is good:\r\n```\r\nStarted PATCH \"/api/v2/my/profile\" for ::1 at 2015-08-27 12:06:18 +0200\r\nProcessing by Api::V2::ProfilesController#update as JSON\r\n  Parameters: {\"my\"=>true, \"profile\"=>{}}\r\nCan't verify CSRF token authenticity\r\n  User Load (0.2ms)  SELECT  \"users\".* FROM \"users\" WHERE \"users\".\"email\" = ? LIMIT 1  [[\"email\", \"user@example.com\"]]\r\n   (0.1ms)  begin transaction\r\n  SQL (0.3ms)  UPDATE \"users\" SET \"last_sign_in_at\" = ?, \"current_sign_in_at\" = ?, \"sign_in_count\" = ?, \"updated_at\" = ? WHERE \"users\".\"id\" = ?  [[\"last_sign_in_at\", \"2015-08-27 10:06:05.720058\"], [\"current_sign_in_at\", \"2015-08-27 10:06:18.609242\"], [\"sign_in_count\", 24], [\"updated_at\", \"2015-08-27 10:06:18.610201\"], [\"id\", 1]]\r\n   (30.0ms)  commit transaction\r\n  Profile Load (0.3ms)  SELECT  \"profiles\".* FROM \"profiles\" WHERE \"profiles\".\"user_id\" = ? LIMIT 1  [[\"user_id\", 1]]\r\n   (0.2ms)  begin transaction\r\n   (0.1ms)  rollback transaction\r\nCompleted 422 Unprocessable Entity in 53ms (ActiveRecord: 31.8ms)\r\n\r\nActiveRecord::RecordInvalid (Validation failed: Name is too long (maximum is 5 characters)):\r\n  app/controllers/api/v2/profiles_controller.rb:6:in `update'\r\n\r\n\r\nProcessing by Api::ErrorsController#show as JSON\r\n  Parameters: {\"my\"=>true, \"profile\"=>{}, \"error\"=>{\"profile\"=>{}}}\r\nCompleted 422 Unprocessable Entity in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms)\r\n```\r\n\r\nHere is call stack from `before_save` in `Profile` https://gist.github.com/olegantonyan/e68999d7c8f6d8f007ba\r\n\r\nTest app: https://github.com/olegantonyan/rails_failed_validations_example\r\nRun db:seed and use test.sh (wrapper for curl) to make a request"},"comment":{"url":"https://api.github.com/repos/rails/rails/issues/comments/174434336","html_url":"https://github.com/rails/rails/issues/21399#issuecomment-174434336","issue_url":"https://api.github.com/repos/rails/rails/issues/21399","id":174434336,"user":{"login":"olegantonyan","id":2301579,"avatar_url":"https://avatars.githubusercontent.com/u/2301579?v=3","gravatar_id":"","url":"https://api.github.com/users/olegantonyan","html_url":"https://github.com/olegantonyan","followers_url":"https://api.github.com/users/olegantonyan/followers","following_url":"https://api.github.com/users/olegantonyan/following{/other_user}","gists_url":"https://api.github.com/users/olegantonyan/gists{/gist_id}","starred_url":"https://api.github.com/users/olegantonyan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/olegantonyan/subscriptions","organizations_url":"https://api.github.com/users/olegantonyan/orgs","repos_url":"https://api.github.com/users/olegantonyan/repos","events_url":"https://api.github.com/users/olegantonyan/events{/privacy}","received_events_url":"https://api.github.com/users/olegantonyan/received_events","type":"User","site_admin":false},"created_at":"2016-01-25T08:21:11Z","updated_at":"2016-01-25T08:21:11Z","body":"Cannot reproduce it with test script."}}