Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No way to handle 404s? #334

Open
antonrd opened this issue Apr 28, 2015 · 20 comments
Open

No way to handle 404s? #334

antonrd opened this issue Apr 28, 2015 · 20 comments

Comments

@antonrd
Copy link

antonrd commented Apr 28, 2015

Is there a way to handle 404 status codes when an object was not found on the other side?

Say that we have a model Post and we have set things up so that we can call Post.find(1) and there is not Post object with id=1 on the other side. Let's say the other API returns nil and status code 404. Her doesn't seem to consider the status code and rather complains with an error about the nil. What is the right way to handle such situations?

@antonrd antonrd closed this as completed Apr 28, 2015
@antonrd
Copy link
Author

antonrd commented Apr 28, 2015

Reopening this as custom paths don't seem to handle 404s well after all. Even when 404 is returned Her prepares a model object but it has not attributes. I would expect it to return nil.

@antonrd antonrd reopened this Apr 28, 2015
@jonathanpa
Copy link

I notice the same thing, I would expect that Her returns nil in case of 40X HTTP codes returns.

@hubert
Copy link
Contributor

hubert commented Jun 4, 2015

i agree that returning nil makes more sense than an object with no attributes.

the place this would be handled is in the Her::Middleware layer. when parsing the object, it's not accounting for 4xx codes as you noticed, and should.

i'm doing some work at this level right now to handle json api compliant apis, so can take a look at this as well, or if you want to take a crack at PRing this yourself, that would be much appreciated.

thanks for the report.

@ak47
Copy link

ak47 commented Jun 9, 2015

the problem with returning nil is you have nothing to work with in the event of an error -
if this is limited to 404's return nil, that might work

but sometimes it's useful to see what was returned, the http status code

ideally, all Her objects would have some simple way to see the http details
like maybe a #_status that gives the code for the call, or maybe even a hash of relevant details
{http_status_code: '404', http_method: 'GET'}

one of the biggest knocks I hear against Her is the lack of transparency when something goes wrong

@dasmoose
Copy link

dasmoose commented Jun 9, 2015

I agree with ak47, sometimes you want to make decisions based on the status code. Like a 401 vs 422 vs 204.

I am working on something right now where on a DELETE of a resource I get a 204 for a successful delete and a 422 for a unsuccessful delete. Now, I have no way of knowing whether or not the call was successful or not on the client side.

Like ak47 said just having a hash of the details would relieve a lot of troubles I have with her.

@hubert
Copy link
Contributor

hubert commented Jun 9, 2015

i agree there needs to be a way to distinguish between things working or not. however, i don't agree that http status codes are the way to do that.

the her middleware layer is there to provide the adapter layer between the http details on the active model object that is returned.

for a 404 specifically, nil is the most obvious, but the tradeoff would be that we wouldn't be able to include any additional info the server provides. the other possibilities would be a "NullModel" of some sort or making find raise an exception akin to how ActiveRecord does it.

for a 422 or other 4xx responses, the way to determine whether or not the delete was success would be to query the errors object attached to the model. Her should probably add a generic error for 4xx cases and then add errors included by the server.

@ak47
Copy link

ak47 commented Jun 9, 2015

object.response_errors => [{:id=>"bb78d697-7b3b-4780-8b45-3c0eadaabf95",
:status=>404,
:title=>"Not found",
:detail=>
"thing not found by key=10a228f9-4dff-4f7d-8aba-1c88fd9a29d8",
:at=>"2015-06-09T22:54:25.943Z"}]

@will-r
Copy link
Collaborator

will-r commented Aug 5, 2015

I think @hubert is right here. System failure (including 401, 403, 404 and 5xx) should raise an exception. In those cases we can't rely on the API response to be sane. Content failure (e.g. 422 because invalid) should change model properties by passing through API error messages.

I would suggest a small family of exceptions that follow the usual status code names (Her::Errors::NotFound, Her::Errors::Unauthorized, Her::Errors::ServerError) and all inherit from Her::Errors::ResponseError.

Associates would sometimes need rescuing from fetch errors. Perhaps that should be configurable:

      belongs_to :country, foreign_key: :country_code, on_error: :raise

For the model errors could I also suggest that we adopt ActiveModel::Errors to try and marshal the received errors hash or array into a standard format? As usual people would need a custom parser if their error format is weird, but the error-processing could be exposed as a separate method.

@marshall-lee
Copy link
Contributor

Custom exceptions are good but theye're not for everyone.
Maybe store object.response_status at least?

@will-r
Copy link
Collaborator

will-r commented Aug 5, 2015

@marshall-lee yes, that would make sense for a processing error. The exceptions are meant for complete failures where we may not even have an object to store errors in. As @antonrd originally pointed out, creating a model object to communicate that there is no model object is too confusing.

To address the original issue report properly: I think the value of a missing object should be nil, and the action of fetching a missing object should raise an exception. Ideally this would be done in a way that is very easy to ignore. For single associations the default behaviour on 404 would be to nullify and continue, but for an explicit find or fetch call we would let the caller handle the exception just as ActiveRecord does.

@marshall-lee
Copy link
Contributor

I think the value of a missing object should be nil

You mean missing association objects? If yes I think the same.

and the action of fetching a missing object should raise an exception

As ActiveRecord throws ActiveRecord::RecordNotFound one would assume a similar behavior for Her. I definitely want this feature but I want it as optional for now. It shouldn't break existing code. For example in our project we handle errors (and not found errors too) by looking into response_errors. We don't like it but it's already implemented and it works, and we don't want our code be broken after upgrading to new version of Her. There should be a graceful upgrade path for existing users.

@will-r
Copy link
Collaborator

will-r commented Aug 5, 2015

Agreed that behaving like ActiveRecord is always going to be comforting. In Her it's easy to configure response error behaviour while setting up an api because in the end all requests are made by Her::API#request. Nice work by Rémi there.

    Her::API.setup url: "https://api.example.com", ignore_response_errors: [404]

The maintainer would have to decide about default behaviour after an upgrade but rails gives us a good example: warn and deprecate one version ahead, then change the default behaviour in a way that is easy to undo.

@marshall-lee
Copy link
Contributor

The maintainer would have to decide about default behaviour after an upgrade but rails gives us a good example: warn and deprecate one version ahead, then change the default behaviour in a way that is easy to undo.

Yeah, I agree.

@alvinschur
Copy link

@wil-r, @hubert I am interested in Her raising an error for unauthorized errors. Or somehow make that case visible.

Is anyone working on related behaviour?

If not I could help out with some guidance.

@hmatsuda
Copy link

@alvinschur Here's my solution.

1 Create faraday middleware to parse JSON data

module ApiResponseHandler
  class UnauthorizedError < StandardError; end

  class CustomerParser < ::Faraday::Response::Middleware
    def call(request_env)
      @app.call(request_env).on_complete do |response_env|
        json = MultiJson.load(response_env[:body], symbolize_keys: true)

        case response_env[:status]
        when 200
          response_env[:body] = {
            data: json,
            errors: [],
            metadata: {}
          }
        when 401
          raise ApiResponseHandler::UnauthorizedError
        end
      end
    end
  end
end

2 Load custom parser instead of default one

# config/initializers/her.rb
Her::API.setup url: "https://api.example.com" do |c|
  c.use ApiResponseHandler::CustomParser
end

3 When REST api returns 401 as http status, it raises ApiResponseHandler::UnauthorizedError

[1] pry(main)> User.find(1)
# raise FaradayApiResponseParser::UnauthorizedError

I hope this helps you.

@alvinschur
Copy link

Thank you for the detailed answer and example.

I am writing several libraries to access internal microservices.

I see a few options for including the error handling code

  1. duplicate the error handling code in each library created to access a microservice
  2. fork her and write the error handling code once
  3. contribute to her so others can benefit from the error handling code

Is it ok to include a middleware as described to handle the unauthorized error in her?

This error handling sounds quite similar to handling 404 and other errors.

Is it feasible to have one middleware handle a list of errors?
Or do people prefer separate middleware, one per error: unauthorized, not found, server error, etc?

@alvinschur
Copy link

  1. a separate project similar to Faraday middleware for the extra middlewares we use

@hmatsuda
Copy link

I don't know best practice how about her's error handling but I'm handling 40x and 50x errors in one middleware(extract gem) like above example code.
If you have serval client application to use same REST API, I think your 4) solution is better.

@SirRawlins
Copy link

Did this conversation go any further? Found myself bumping up against it today, expecting find(unknown id) to Raise an exception but it swallows it.

@coorasse
Copy link

I am adding my solution here.
Our models do not include Her::Model directly but they all inherit from an ApplicationModel that includes Her::Model.
This gives us the possibility to override methods for all our models.
We did it for different things and one of these was to handle 404.

What we did is as simple as:

class ApplicationModel
  include Her::Model
  
  def self.find(params)
    result = super
    raise ActiveRecord::RecordNotFound unless result
    result
  end
  
  # other stuff overrridden
end

I hope this helps somebody else

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests