piratetone
piratetone

Reputation: 1319

Rails: POST 422 (Unprocessable Entity) in Rails? Due to the routes or the controller?

I'm trying to give users on my website "points" or "credits" for tweeting about out the brand name.

I have the fancy twitter widget on the appropriate view...

<p><a  href="https://twitter.com/share" class="twitter-share-button" data-text="Check Out This Awesome Website Yay" data-via="BrandName" data-hashtags="ProductName">Tweet</a>
<div id="credited"></div>
<script>window.twttr = (function (d, s, id) {
  var t, js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src= "https://platform.twitter.com/widgets.js";
  fjs.parentNode.insertBefore(js, fjs);
  return window.twttr || (t = { _e: [], ready: function (f) { t._e.push(f) } });
}(document, "script", "twitter-wjs"));
</script>    

I have the JS all written up and pretty....

function creditTweet() {
  $.post(
    "/credit_tweet",
    {},
    function(result) {
      var text;
      if (result.status === "noop") {
        text = "Thanks for sharing already!";
      } else if (result.status === "ok") {
        text = "5 Kredit Added";
      }
      $("#credited").html(text);
    }
  );
}

$(function() {
  twttr.ready(function (twttr) {
    window.twttr.events.bind('tweet', creditTweet);
  }); 
});

Now the problem is either in the controller OR in the routes (where I'm posting). I think the routes are fine because the POST is almost working, because this is the description of the error on wikipedia - "422 Unprocessable Entity (WebDAV; RFC 4918) The request was well-formed but was unable to be followed due to semantic errors."

So, do you guys see anything wrong with my ruby code in the controller?

class SocialKreditController < ApplicationController
    TWEET_CREDIT_AMOUNT = 5

  def credit_tweet
    if !signed_in?
      render json: { status: :error }
    elsif   current_user.tweet_credited
        Rails.logger.info "Not crediting #{ current_user.id }"
        render json: { status: :noop }
      else
        Rails.logger.info "Crediting #{ current_user.id }"
        current_user.update_attributes tweet_credited: true
        current_user.add_points TWEET_CREDIT_AMOUNT
        render json: { status: :ok }
      end
  end
end

And in my routes.rb, it's pretty straight forward, so I doubt there's anything wrong here...

  get 'social_kredit/credit_tweet'
  post '/credit_tweet' => 'social_kredit#credit_tweet'

Where oh where is this error? I clearly don't know smack about HTTP requests.

Upvotes: 34

Views: 98277

Answers (6)

Dorian
Dorian

Reputation: 9105

a 422 can happen when updating a model in a POST request:

e.g. @user.update!(email: nil) in users#update will make a 422 if there is a validation on email like validates :email, presence: true

Upvotes: 1

Promise Preston
Promise Preston

Reputation: 29008

I had this challenge when working on a Rails 6 API-only application.

I followed the answer here - Rails: How to implement protect_from_forgery in Rails API mode to implement protect_from_forgery in Rails API mode, but I was still having the Can't verify CSRF token authenticity error followed by the 422 Unprocessable Entity error when I send post requests from Postman:

Started POST "/api/v1/programs" for ::1 at 2021-02-23 18:42:49 +0100
Processing by Api::V1::ProgramsController#create as JSON
  Parameters: {"program"=>{"name"=>"Undergraduate", "code"=>"UD", "affiliate_status"=>false, "motto"=>"Our motto is ...", "description"=>"This is a new program", "school_id"=>1}}
Can't verify CSRF token authenticity.
  TRANSACTION (0.3ms)  BEGIN
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
  Baserecord::School Load (0.3ms)  SELECT "schools".* FROM "schools" WHERE "schools"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
  TRANSACTION (0.4ms)  ROLLBACK
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
Completed 422 Unprocessable Entity in 30ms (Views: 0.8ms | ActiveRecord: 6.9ms | Allocations: 13172)

Here's I solved it:

The issue was caused by a validation error from my models. A validation I added to the model for the controller I was calling was failing. I had to check the Body of the response returned by Postman after making the request to find the error:

{
    "affiliate_status": [
        "can't be blank"
    ]
}

Once I fixed the error following this answer - Rails: Validation fails for ActiveRecord in setting a random Boolean Attribute, everything worked fine afterward.

That's all.

I hope this helps

Upvotes: 1

barlop
barlop

Reputation: 13780

ihaztehcodez(who was last active in 2016 so it won't help nudging him to post an answer) mentions that the skip_before_action :verify_authenticity_token technique is not so secure 'cos you lose forgery protection.

they mention that the best/secure/'better practise', solutions are mentioned here WARNING: Can't verify CSRF token authenticity rails

e.g.

$.ajaxSetup({
  headers: {
    'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
  }
});

or

$.ajax({ url: 'YOUR URL HERE',
  type: 'POST',
  beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
  data: 'someData=' + someData,
  success: function(response) {
    $('#someDiv').html(response);
  }
});

or

putting this within an ajax request

headers: {
  'X-Transaction': 'POST Example',
  'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},

Upvotes: 6

Lex
Lex

Reputation: 5014

If you're including Rails meta data in the HTML header with <%= csrf_meta_tags %> it'll generate the following.

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="ihwlaOLL232ipKmWYaqbSZacpJegQqooJ+Cj9fLF2e02NTQw7P/MfQyRuzruCax2xYWtEHWsb/uqiiZP6NWH+Q==" />

You can pull the CRSF token from the meta data and pass it into your async request. Using the native js fetch method you can pass it in as a x-csrf-token header.

This is a trimmed onSave handler for a React component that enhances a standard Rails form.

  onSaveHandler = (event) => {
    const data = "Foo Bar";
    const metaCsrf = document.querySelector("meta[name='csrf-token']");
    const csrfToken = metaCsrf.getAttribute('content');
    fetch(`/posts/${this.props.post_id}`, {
      method: "PUT",
      body: JSON.stringify({
        content: data
      }),
      headers: {
        'x-csrf-token': csrfToken,
        'content-type': 'application/json',
        'accept': 'application/json'
      },
    }).then(res => {
      console.log("Request complete! response:", res);
    });
  }

Forgery protection is a good idea. This way we stay secure and don't mess with our Rails configuration.

Using gem 'rails', '~> 5.0.5' & "react": "^16.8.6",

Upvotes: 0

Shekhar Patil
Shekhar Patil

Reputation: 337

Same problem I faced. It sorts out after adding

skip_before_action :verify_authenticity_token

at the top of your controller where your JS is calling or sending data.

class UserController < ApplicationController
    skip_before_action :verify_authenticity_token
    def create
    end
end

as shown in code snippet.

Upvotes: 2

piratetone
piratetone

Reputation: 1319

I got it working!

I added a...

skip_before_action :verify_authenticity_token

to the controller.

The issue was found when checking out the logs and seeing that the CSRF token could not be verified.

Upvotes: 76

Related Questions