framework: Route model binding parameter error passing a string with Postgres

  • Laravel Version: 5.7.9
  • PHP Version: 7.2.10
  • Database Driver & Version: PostgreSQL 10.5

Description:

I noticed that the route model binding exists an error passing a string variable when using the Postgres database, this does not happen with the MySQL database. When I use the Postgres database, the route model binding if pass a parameter string it don’t abort and build the query with a string. When I use the MySQL database it abort and show error 404 the route is not found.

Steps To Reproduce:

Records

id name
1 John
2 Jean
3 Lian

Routes

Route::get('/user/{user}', 'UserController@show');

UserController

public function show(User $user) {
    return $user;
}

url abort

localhost/user/4

url does not abort

localhost/user/test

Error

SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for integer: "test" (SQL: select * from "users" where "id" = test limit 1)

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 4
  • Comments: 18 (10 by maintainers)

Commits related to this issue

Most upvoted comments

That would do the trick, sure, but it feels weird that you need to consider it (and litter your routes file with constraints) just because you’re using Postgres.

Do you have an idea for a fix?

We could wrap the query in a try/catch block and throw a ModelNotFoundException, but I don’t think that’s ideal. This makes it harder debug other issues like missing tables or misspelled columns.

Or we use the model’s $keyType and check integer keys with ctype_digit() (if getRouteKeyName() === getKeyName()).

@dwightwatson mariohbrino’s suggestion is checking that:

`$instance->getRouteKeyName() === $instance->getKeyName()`

You’ll find Postgres has similar “Invalid text representation” exceptions for UUID columns that don’t allow any SQL integer or string value in the SELECT parameter binding. So RouteServiceProvider does turn into a regex fest:

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    protected $namespace = 'App\Http\Controllers';

    protected const MODEL_ID_BINDINGS = [
        'id',
        'notification',
        // ...
    ];

    protected const MODEL_UUID_BINDINGS = [
        'uuid',
        'photo',
        'user',
        // ...
    ];

    public function boot()
    {
        Route::patterns(array_merge(
            array_fill_keys(self::MODEL_ID_BINDINGS, '[0-9]{1,10}'),
            array_fill_keys(self::MODEL_UUID_BINDINGS, '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'),
            [
                // ...
            ]
        ));

        parent::boot();
    }

Instead of inferring from Model@getKeyType() A possible model-focused solution is to add getRouteKeyType() to indicate the type or pattern of getRouteKeyName():

public function getRouteKeyName()
{
    return 'uuid';
}

public function getRouteKeyType()
{
    return '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
}
public function getRouteKeyName()
{
    return 'id';
}

public function getRouteKeyType()
{
    return 'int';
}

Now route pattern matching is spread across many model classes instead of all being located in RouteServiceProvider. However it would allow a project base model class to define everything in one spot without having to name the route bindings:

namespace App;

use Illuminate\Database\Eloquent\Model as BaseModel;

class Model extends BaseModel
{
    public function getRouteKeyType()
    {
        switch ($this->getRouteKeyName()) {
            case 'id':
                return '\d+';
            case: 'uuid':
                return '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
            default:
                break; // no pattern enforced
        }
    }
}

I truthfully don’t see many problems with the current implementation other than a dozen or so explicit regex pattern lines? Do the docs just need explicit instructions for Postgres columns?

I’m facing the same issue in 2021 with Laravel 8. Really easy solution why not wrap the value in the query in back-ticks? Solves the issue for everyone. I wanted to return json for a Route::resource for which I use the ->missing() feature, but if a user passes in a string instead of an int I get the postgres related error. So I tried Route::pattern but this returns a html 404.

My solution is to overwrite your implicit findOrFail with the following in my model and it will work every time, sending back my ->missing json return statement

public function resolveRouteBinding($value, $field = null)
    {
        return $this->where('id', `$value`)->firstOrFail();
    }

passing user submitted inputs to backticks is a seriously bad idea

you leave yourself vulnerable to shell injection / remote code execution

I’m facing the same issue in 2021 with Laravel 8. Really easy solution why not wrap the value in the query in back-ticks? Solves the issue for everyone. I wanted to return json for a Route::resource for which I use the ->missing() feature, but if a user passes in a string instead of an int I get the postgres related error. So I tried Route::pattern but this returns a html 404.

My solution is to overwrite your implicit findOrFail with the following in my model and it will work every time, sending back my ->missing json return statement

public function resolveRouteBinding($value, $field = null)
    {
        return $this->where('id', `$value`)->firstOrFail();
    }

You also have to check the $keyType for int/integer. Otherwise, this breaks string primary keys.