10

I started using Postgres UUID type for all my models' id fields. Works great and is supported (for the most part) in Rails 4:

create_table :users, id: :uuid do |t|
  # ...
end

The problem is that Postgres will raise an error if you attempt to find a row where id is X, but X is not a properly formatted UUID string.

> User.find "3ac093e2-3a5e-4744-b49f-117b032adc6c"
ActiveRecord::RecordNotFound # good, will cause a 404
> User.find "foobar"
PG::InvalidTextRepresentation: ERROR # bad, will cause a 500

So if my user is on a page where a UUID is in the URL, and they then try to change the UUID, they'll get a 500 error instead of 404. Or perhaps they get a link to an object that no longer exists.

How can I go about avoiding this scenario in a DRY way? I can't just rescue the PG::InvalidTextRepresentation and render 404 because other things can cause this error as well.

UPDATE

I think that a regex on the format of the ID param is clean, and it raises a 404 if it doesn't match:

resources :users, id: /uuid-regex-here/

But I still have the problem of staying DRY; I don't want to put this on every single resource in my routes. I can declare multiple resources in one statement, but only if don't other options to it like member actions. So perhaps a better question is: Is there a way to set the id regex for all routes?

15
  • Why are you passing foobar, though? Shouldn't your route catch that before it even lands in the hands of your model? Commented Jan 24, 2014 at 22:05
  • @Denis I suppose I could put a constraint on the id param ensuring it matches a UUID regex. But I would have to do that for every single resource in my routes... or did you have something else in mind? Commented Jan 24, 2014 at 22:19
  • Perhaps a before_filter for your controllers would work. Commented Jan 24, 2014 at 22:27
  • @tybro0103: Speaking personally, if I forgot to make my own routes validate that the id is of the right format, or if some random call somewhere sent an id in the wrong format, I'd actually want them to cough an error. I'm not familiar enough with Rails to say exactly how you could mass-auto-validate them, but I'm guessing mu is onto something with his suggestion. Commented Jan 24, 2014 at 22:30
  • @Denis: The underlying problem is that the Rails guys are too lazy to put validation in the right place, they're assuming that Model.find will raise a RecordNotFound if the record is not found for any reason (including a type error such as M.find('pancakes') when the PK is an integer). In the Rails model, adding constraints to the routes would probably be the Right Thing, a filter is a quick hack. Rails is rife with unstated assumptions (try adding a route that contains an email address and see what happens) and it goes sideways when you stray from the conventional path. Commented Jan 24, 2014 at 22:43

2 Answers 2

7

You can add a routing constraint to multiple routes at a time via constraints() do ... end.

I ended up doing this and setting a global constraint on all :id params to match it to a UUID regexp:

MyApp::Application.routes.draw do
  constraints(id: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i) do

    # my routes here

  end
end

This way, /posts/123 or /posts/foobar no longer match /posts/:id and 404 before ever invoking the controller action, thus avoiding the PG type error.

All of my models will use UUID for their IDs so this is clean and DRY. If I had some models with integer IDs as well, it'd be a little less clean.

Sign up to request clarification or add additional context in comments.

3 Comments

If you're using the UUID gem, you can use their validate method instead of rolling your own github.com/assaf/uuid/blob/master/lib/uuid.rb#L199
@BrianHahn Good to know. Though I'm not really rolling my own... it's just a regexp.
I meant it'd be rolling your own in the context of already having the UUID gem. Your regexp seems fine when the UUID gem isn't a dependency in your project!
2

If you don't want to add constraints to all the routes to catch invalid UUIDs then you could kludge in a before_filter, something like this:

before_filter do
  if(params.has_key?(:id))
    uuid = params[:id].strip.downcase.gsub('-', '').gsub(/\A\{?(\h{32})\}?\z/, '\1')
    raise ActiveRecord::RecordNotFound if(uuid.blank?)
  end
end

Note that UUIDs can come in various forms (see the fine manual) so it is best to normalize them before validating them or do both normalization and validation at the same time.

You could put that into your ApplicationController if you know that all your :id parameters are supposed to be UUIDs or put the logic in an ApplicationController method and before_filter :make_sure_id_is_a_uuid in the controllers that need it.

2 Comments

This isn't bad. I'll end up going with this if I don't find a way to add the regex to the routes only once.
Another monkey patching possibility would be the type conversion stuff inside the AR PostgreSQL driver, there's a big case statement somewhere in there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.