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

Refactor errors #213

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Refactor errors #213

wants to merge 13 commits into from

Conversation

pedro
Copy link

@pedro pedro commented Sep 16, 2015

  • Constructors take options hash
  • Support for metadata (new error attributes that are serialized)
  • Automatically render 404s for Sequel::NoMatchingRow errors

@@ -108,6 +109,7 @@ class GatewayTimeout < HTTPStatusError; end # 504
UnprocessableEntity => [422, 'Unprocessable entity'],
TooManyRequests => [429, 'Too many requests'],
InternalServerError => [500, 'Internal server error'],
HTTPStatusError => [500, 'Unspecified'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pedro The original idea here was that HTTPStatusError would behave more like an abstract class which would never be instantiated directly; which is also why it didn't get an entry in this table. What do you think of changing this to just raising an ArgumentError if the instantiated error class wasn't found in META?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, makes sense! changing it.

@brandur
Copy link
Member

brandur commented Sep 18, 2015

@pedro Nice job. Left a few minor comments, but +1 from me!

@gudmundur
Copy link
Member

@pedro I'd love to see this merged. Is there anything that needs to be fixed or can it be merged?

Pedro Belo added 5 commits January 5, 2016 17:30
@pedro
Copy link
Author

pedro commented Jan 6, 2016

@brandur @gudmundur hey folks, good time to follow up on this – I just had to refactor how we handle errors at Lugg and ended up with a mix of Pliny and new stuff we're super happy about.

It's not a huge departure from this. This basically highlights the issue:

Having exceptions take a message as their first argument is a fairly widespread convention.

Ruby exceptions are meant to have a message, but we all know API errors really just need an id. They are easier to compare in tests, and they won't pollute code with copy that we all know should live under config/locales.yml.

But no one said that the Ruby exception message is what we render to end-users. "invalid_json" works perfectly fine as the internal exception message, and it also happens to be a good error identifier for Pliny APIs, not to mention a good key to lookup the corresponding entry in the locales file.

So here's the proposal: Pliny ships with i18n. All existing user error messages are set there (like "Unauthorized" and "Unprocessable entity"). Our base error class only requires an id, which is used to fetch these messages and save them as the error.user_message. The id is also stored internally as the exception message so you can raise them using the same interface, like raise Pliny::Errors::BadRequest("invalid_json").

You can then test for these exceptions using standard matchers, like:

expect { run }.to raise_error(Pliny::Errors::BadRequest, "invalid_json")

Finally, while in there I'd also like to remove all exception classes outside 4xx and 5xx. Because if you want to render a 30x, for instance, you should rely on Sinatra's helpers and not on a Pliny exception.

Thoughts?

@mattreduce
Copy link

@pedro 💯 👍

@brandur
Copy link
Member

brandur commented Jan 6, 2016

Thoughts?

I like it (both taking the messages out with default localization and removing non-4/5xx series exceptions). +1.

@dmathieu
Copy link
Contributor

dmathieu commented Jan 6, 2016

I really like this idea as well.

@pedro
Copy link
Author

pedro commented Jan 7, 2016

you folks rock, thanks for the feedback! updated to match then 💃

Pedro Belo added 2 commits January 6, 2016 20:18
@dmathieu
Copy link
Contributor

dmathieu commented Jan 7, 2016

This is looking nice. The only comment I have is from a non-native english perspective. If I were to wish to translate error messages, I wouldn't be able to do the following:

error = Pliny::Errors::BadRequest.new
I18n.locale = :fr
error.user_message #=> will show the error in english

It's probably an edge case though.

class BadGateway < HTTPStatusError; end # 502
class ServiceUnavailable < HTTPStatusError; end # 503
class GatewayTimeout < HTTPStatusError; end # 504
def self.MakeError(status, id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this method name be snakecased?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh god, yes. I think I saw this class-looking method used before for a similar purpose but looking now it's super confusing. Fixed!

@brandur
Copy link
Member

brandur commented Jan 7, 2016

@dmathieu Should be easy enough to just move the translation over to be execute on access of user_message, but I guess you do introduce a slight risk of making rendering an error multiple times more expensive. IMO, either deficiency in likely to cause too much of a problem in practice, so will leave it up to Pedro :)

@pedro Left a minor comment above, but looks good to me! +1.

@dmathieu
Copy link
Contributor

dmathieu commented Jan 7, 2016

My thinking was more to actually lazily fetch this data (and maybe memoize it to avoid expensive i18n lookup calls).

def user_message
  @user_message ||= I18n.t("errors.#{id}")
end

@brandur
Copy link
Member

brandur commented Jan 8, 2016

My thinking was more to actually lazily fetch this data (and maybe memoize it to avoid expensive i18n lookup calls).

Ah! I guess the only thing there is that with that implementation you could produce another failure scenario that's not too different from the one you have above:

error = Pliny::Errors::BadRequest.new
error.user_message #=> memoized
I18n.locale = :fr
error.user_message #=> will show the error in english

Although admittedly, also probably unlikely in practice.

Pedro Belo added 2 commits January 7, 2016 16:54
facilitate error messages in other languages
@pedro
Copy link
Author

pedro commented Jan 8, 2016

good point. moved to a method not memoized for now, don't think we should worry too much about optimizing exceptional paths.

@dmathieu
Copy link
Contributor

dmathieu commented Jan 8, 2016

👍

@jellybob
Copy link

jellybob commented Jan 8, 2016

If that's actually an issue in practice could you memoize keyed against the current locale, something like #{I18n.locale}_#{key} which would then be invalidated by changing the locale?

class SeeOther < HTTPStatusError; end # 303
class NotModified < HTTPStatusError; end # 304
class UseProxy < HTTPStatusError; end # 305
class TemporaryRedirect < HTTPStatusError; end # 307
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that removing all the non 4xx-5xx error classes is going to cause things to break in a few of my apps. To make matters a little worse, I suspect a few of them might be used for control flow.

That said, I do think this is a good change and what I've been doing is BAD (tm).

@pedro
Copy link
Author

pedro commented Jan 16, 2016

@gudmundur nod on the breaking changes – this would obviously require a major version bump normally, but seeing that we're still on the 0.x series I'm not sure there's much else we can do. Thoughts?

@brandur
Copy link
Member

brandur commented Jan 19, 2016

@pedro It's kind of a departure from the proposal above, should we allow an override message to be given for backward compatibility (and make that route deprecated, or at least unusual)? I guess the disadvantage is developers not knowing that they usually shouldn't provide it, but that's not a huge downside.

Definitely like the original proposal as is, but given that Pliny's seeing a reasonable amount of use now, erring on the side of conservative interface changes is probably a good idea.

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

Successfully merging this pull request may close these issues.

6 participants