Return error as a JSON response - ruby-on-rails

The question is simple: return an error from a controller as JSON response:
//controller
def action1
# .....
return render(json: { status: :unprocessable_entity }) if !is_my_valid?
# .... doing something
render(json: my_hash.to_json)
end
//view
$.ajax({
url: 'some_url',
//...
}).done(function(data) {
// it always gets here even in case of an error
}).fail(function(jqXHR, textStatus) {
alert("error: " + textStatus);
});
So the problem is that on the HTML page, even in case of an error, the callback function in .done() is executed, not the one in .fail(). How do I make it into .fail()?

Send back a 500 HTTP status code:
render(json: { ... }, status: 500)

Related

Fetch request on API Rails doesn't return json

In a Rails API, I have a login POST method in my UsersController which takes 2 parameters (mail and password) and check in DB if a record is found and if so returns it as JSON.
def login(mail, password)
mail, password = params.values_at(:mail, :password)
user = User.where(mail: mail, password: password)
render json: user
end
In my front side, in React, I call this method with fetch which takes the mail and password values in a form and expect to have the user as JSON in my 'res':
login = () => {
if(this.state.mail != null && this.state.password != null){
fetch('http://127.0.0.1:3001/api/login', {
method: 'post',
body: JSON.stringify({
mail: this.state.mail,
password: this.state.password
}),
headers: {
'Accept': 'application/json',
'Content-type': 'application/json'
}
})
.then((res) => {
console.log(res)
if(res.data.length === 1 ){
const cookies = new Cookies();
cookies.set('mercato-cookie',res.data[0].id,{path: '/'});
this.setState({redirect: true})
}
})
} bodyUsed: false
headers: Headers { }
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://127.0.0.1:3001/api/login"
__proto__: ResponsePrototype { clone: clone(), arrayBuffer: arrayBuffer(), blob: blob(), … } auth.js:32
}
Problem is my res doesn't correspond to what I return with render json: user, so I made a console.log(res) :
Response
bodyUsed: false
headers: Headers { }
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://127.0.0.1:3001/api/login"
__proto__: ResponsePrototype { clone: clone(), arrayBuffer: arrayBuffer(), blob: blob(), … } auth.js:32
I tried returning simple JSON text in case there was a problem with my user variable and also tried changing render json: user to format.json { render json: user } but with no result :/
I made the request on Postman and it returns the appropiate JSON, so i guess the problem comes from my fetch ?
Fetch's response doesn't automatically translate to JSON, you need to call response.json() (which returns a promise) in order to get the JSON value. See this example from MDN, or here's some ES6 to match your code:
fetch(myRequest)
.then(response => response.json())
.then((data) => {
// I'm assuming you'll have direct access to data instead of res.data here,
// depending on how your API is structured
if (data.length === 1) {
const cookies = new Cookies();
cookies.set('mercato-cookie', data[0].id, {path: '/'});
this.setState({redirect: true});
}
});

Ruby Devise sign up with AJAX not getting json response?

I have followed this tutorial: https://blog.andrewray.me/how-to-set-up-devise-ajax-authentication-with-rails-4-0/ And am using rails 5.1.
I have implemented the json response in the controller:
class RegistrationsController < Devise::RegistrationsController
respond_to :json
end
And when I call the ajax I only get an Html/Text response:
function createUser(callback) {
$.ajax({
type: "POST",
url: window.urls.createUser,
data: {
authenticity_token: $("meta[name=csrf-token]").attr("content"),
user: grabOrderFormUserData()
},
success: function(data) {
console.log("Data: " + data);
},
error: function (data) {
//console.log("error");
}
})
}
That call works fine, but returns the HTML page of the sign up.
The url I use is createUser: hostUrl + '/users/'
What did I miss?
And no, if I add .json to my url, it will respond with 500 error code.
I think you missed to mention dataType: "json" with your ajax call , try this
function createUser(callback) {
$.ajax({
url: window.urls.createUser,
type: "POST",
data: {`enter code here`
authenticity_token: $("meta[name=csrf-token]").attr("content"),
user: grabOrderFormUserData()
},
dataType: "json",
success: function(data) {
console.log("Data: " + data);
},
error: function (data) {
//console.log("error");
}
})
}
and also in your controller try to call this block in case if you are not getting json response,
respond_to do |format|
format.json {
render json: {.....}
}
end
thank you.

How to respond to ajax call in Rails

I know there are many questions about that already on stackoverflow but none of them has been useful for me. Here is my ajax code:
function update_schedule($task){
var taskId = $task.data("taskId"),
startHour, endHour,
userId = $('#users').val();
if( !$task.hasClass('daily-task')){
startHour = getStartHour($task),
endHour = startHour + (+$task.data('time'));
}
console.log(startHour, endHour)
$.ajax({
url: '/heimdall/update_scheduled_task',
method: 'POST',
data: { "task_id": taskId, "start_hour": startHour, "end_hour": endHour, "user_id": userId },
success: function (){
console.log("SUCESS!!", data['head'])
},
error: function () {
console.log("FAILURE");
},
async: true
});
}
The controller code:
def update_scheduled_task
scheduled_task = ScheduledTask.find_or_create_by(task_id: params[:task_id])
scheduled_task.update_attributes(start_hour: params[:start_hour], end_hour: params[:end_hour], task_id: params[:task_id], user_id: params[:user_id])
end
I want to return the id of found or created task. I don't know how to send/receive this information. I've read about respond to but still I don't know how to use it in this case.
You may do render json: ... to return the required info to ajax in JSON format:
def update_scheduled_task
scheduled_task = ScheduledTask.find_or_create_by(task_id: params[:task_id])
scheduled_task.update_attributes(start_hour: params[:start_hour], end_hour: params[:end_hour], task_id: params[:task_id], user_id: params[:user_id])
render json: {scheduled_task_id: scheduled_task.id}
end
And in the ajax function's success, use it like:
success: function (data){
var data = JSON.parse(data);
console.log(data["scheduled_task_id"]);
},
First, you need to improve your controller code structure (You can rely on the default scaffold generator methods). Then, you must indicate that your method will respond to json format only, with the answer you want to return, something like this:
def update_scheduled_task
scheduled_task = ScheduledTask.find_or_create_by(task_id: params[:task_id])
if (scheduled_task && scheduled_task.update_attributes(start_hour: params[:start_hour], end_hour: params[:end_hour], task_id: params[:task_id], user_id: params[:user_id]))
render json: { scheduled_task_id: scheduled_task.id }
else
render json: { error: l18n.t("error.messages.request_error") }
end
end
Then, you must modify the success and failure response methods of the jquery ajax request. Here's an example of how it might look:
$.ajax({
url: "/heimdall/update_scheduled_task",
method: "post",
data: { task_id: taskId, start_hour: startHour, end_hour: endHour, user_id: userId },
success: function(result) {
if (result["scheduled_task_id"] != null) {
console.log("Schedule record => " + result["scheduled_task_id"])
} else {
console.log("Error: " + result["error"])
}
},
error: function() {
console.log("Ajax request error");
},
// async: true => By default JQuery set true to async param.
});
Do not forget that you need to add a rule to access this method in the file config/ruotes.rb, something like this:
post update_scheduled_task, :defaults => { :format => 'json' }
I hope this helped you, regards!
jQuery's success and error callbacks rely on the status code returned by the controller. Make sure that you return the right status code when you are unable to create/read/update the object. On success, simply render a JSON with the id property set. I beleive update_attributes returns true on success:
if scheduled_task && scheduled_task.update_attributes(...)
render json: { id: scheduled_task.id }
else
head :unprocessable_entity
end

Render json with status update 401

I'm trying to add a before_action that checks for a user and returns json to the angular app with a status of 401 if there is no user.
the function is simply
def authenticate_user
if !logged_in
render json: {:status => 401}
end
end
However, this never hits my httpInterceptor and renders the json straight to the page. How can I get this response to return with the proper headers so the interceptor can handle a 401?
app.run(function($http, $cookies, $rootScope, $location) {
$http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
$http.defaults.headers.common['Content-Type'] = 'application/json';
});
app.factory('myHttpInterceptor', ['$cookies', '$location', '$window',
function($cookies, $location, $window) {
return {
'request': function(config) {
return config
},
'response': function(response) {
if ($location.$$path !== '/' && !$cookies.csrftoken){
$location.path('/');
}
return response
}
}
}
]);
Did you add your interceptor to the httpProvider ?
app.config(function($httpProvider) {
$httpProvider.interceptors.push('myHttpInterceptor');
})
See there : https://docs.angularjs.org/api/ng/service/$http

angular resource not invoking my error callback function

I've been at this for hours and I can't figure out why angular is not triggering my error call back when my rails back-end raises a proper error. I'm using angular 1.2.0rc1.
According to the documentation:
non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
And I'm using it in my angular controller during a save product operation:
$scope.saveProduct = function(product){
if (product.id) {
Product.update({id: product.id},{product: product}, function(data){
console.log('handle success')
}, function(){
console.log('handle error') //THIS IS NEVER OUTPUT!
});
}
}
Here is the resource definition:
angular.module('sellbriteApp.resources').factory('Product', function ($resource) {
return $resource('/api/products/:id', { id: "#id" },
{
'create': { method: 'POST' },
'index': { method: 'GET', isArray: true },
'show': { method: 'GET', isArray: false },
'update': { method: 'PUT' },
'destroy': { method: 'DELETE' }
}
);
});
Here is my rails controller:
def update
respond_to do |format|
if #product.update(product_params)
format.html { redirect_to [:edit, #product], notice: 'Product was successfully updated.' }
format.json { render 'products/show.json.jbuilder', status: :accepted }
else
format.html { render action: 'edit' }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
Rails returns a 422 status when attempting to save a product with a duplicate sku, and I want to display an error msg on the front end.
I would expect that angular should execute the error handling function provided in the update call, but I can't get that far. Instead in my console I see:
TypeError: Cannot read property 'data' of undefined with an unhelpful stacktrace:
TypeError: Cannot read property 'data' of undefined
at $http.then.value.$resolved (http://localhost:9000/bower_components/angular-resource/angular-resource.js:477:32)
at wrappedCallback (http://localhost:9000/bower_components/angular/angular.js:9042:59)
at wrappedCallback (http://localhost:9000/bower_components/angular/angular.js:9042:59)
at http://localhost:9000/bower_components/angular/angular.js:9128:26
at Object.Scope.$eval (http://localhost:9000/bower_components/angular/angular.js:9953:28)
at Object.Scope.$digest (http://localhost:9000/bower_components/angular/angular.js:9809:23)
at Object.$delegate.__proto__.$digest (<anonymous>:844:31)
at Object.Scope.$apply (http://localhost:9000/bower_components/angular/angular.js:10039:24)
at Object.$delegate.__proto__.$apply (<anonymous>:855:30)
at done (http://localhost:9000/bower_components/angular/angular.js:6542:45)
What am I missing?
UPDATE:
Apparently this http interceptor is related. If I comment this code out, the error function is called. I had copied this snippet from some where else and modified it in order to redirect a user to the sign_up page if they hit a rails api when they are not logged in. It must be interfering, but I'm not sure how I should fix it.
App.config(['$httpProvider', function ($httpProvider) {
$httpProvider.responseInterceptors.push('securityInterceptor');
}]);
App.factory('securityInterceptor', ['$injector', '$location', '$cookieStore', function ($injector,$location,$cookieStore) {
return function(promise) {
var $http = $injector.get('$http');
return promise.then(null, function(response){
if (response.status === 401) {
$cookieStore.remove('_angular_devise_merchant');
toastr.warning('You are logged out');
$location.path('/sign_in');
}
});
};
}]);
You need to reject the promise in your interceptor as well, otherwise its considered as if you've 'handled' the exception.
So:
App.factory('securityInterceptor', ['$injector', '$location', '$cookieStore', '$q', function ($injector,$location,$cookieStore, $q) {
return function(promise) {
var $http = $injector.get('$http');
return promise.then(null, function(response){
if (response.status === 401) {
$cookieStore.remove('_angular_devise_merchant');
toastr.warning('You are logged out');
$location.path('/sign_in');
}
return $q.reject(response);
});
};
}]);

Resources