Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid PG::InvalidTextRepresentation error when using Postgres UUID in Rails

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?

like image 363
tybro0103 Avatar asked Jan 24 '14 17:01

tybro0103


2 Answers

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.

like image 66
tybro0103 Avatar answered Oct 28 '22 20:10

tybro0103


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.

like image 2
mu is too short Avatar answered Oct 28 '22 20:10

mu is too short