Skip to content

[13.x] feat: convert QueryException to ModelNotFoundException in implicit route binding#58943

Draft
calebdw wants to merge 1 commit into
laravel:13.xfrom
calebdw:calebdw/push-sxosyvyswkqo
Draft

[13.x] feat: convert QueryException to ModelNotFoundException in implicit route binding#58943
calebdw wants to merge 1 commit into
laravel:13.xfrom
calebdw:calebdw/push-sxosyvyswkqo

Conversation

@calebdw

@calebdw calebdw commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Hello!

When implicit route model binding encounters a QueryException (e.g., from integer overflow or type mismatch), convert it to a ModelNotFoundException instead of letting the 500 propagate. This ensures invalid route parameters like /users/99999999999999999999 return a 404 rather than a 500.

This has been the biggest pain, without this you have to add ->whereNumber() on every route OR you litter the AppServiceProvider with 50+ calls like Route::pattern('user', '[0-9]+');. Even with this, we've still had exceptions where the parameter was all digits but it exceeded the column int size so we had to add even more checks to prevent the exception.

Ultimately, a model was not found so end user should always receive a 404 instead of a 500.

The QueryException is still reported, but developers now have the ability to disable reporting, for example:

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        Model::reportRouteModelBindingExceptions(! $this->app->isProduction());
    }
}

Thanks!

@calebdw calebdw changed the title [13.x] feat: convert QueryException to ModelNotFoundException in implicit route binding [13.x] feat: convert QueryException to ModelNotFoundException in implicit route binding Feb 20, 2026
@calebdw calebdw force-pushed the calebdw/push-sxosyvyswkqo branch from ad0a9b6 to e484dc0 Compare February 20, 2026 14:13
@shaedrich

Copy link
Copy Markdown
Contributor

Is this intentional? 🤔

@calebdw

calebdw commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Is this intentional? 🤔

Is what intentional? 🧐 I intentionally submitted this PR if that's what you're asking about 🙃

@shaedrich

Copy link
Copy Markdown
Contributor

No, sorry, should have included the quote, I was looking at 😬 😅

This ensures […] a 404 rather than a 500.

@calebdw

calebdw commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Lol, I'm not sure. I was trying to keep this PR simple, but if folks want the exception then I can add a way to configure it

@shaedrich

Copy link
Copy Markdown
Contributor

I guess, that would be great—thanks 👍🏻

@taylorotwell

taylorotwell commented Feb 20, 2026

Copy link
Copy Markdown
Member

I wonder if this would be hard to debug what is going on, especially if you expect to be getting a model back, you get some query error (maybe a global scope), but now it's masked as a 404. It feels like the current behavior is pretty helpful in local development, but maybe not ideal in prod (though you would still want the original error).

@taylorotwell taylorotwell marked this pull request as draft February 20, 2026 15:27
@calebdw

calebdw commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, I could see maybe having some configuration options, maybe something like:

Model::throwRouteModelBindingExceptions();
// and / or maybe just this
Model::reportRouteModelBindingExceptions();

I think from the end user perspective they should always receive a 404 instead of a 500 in production

@shaedrich

Copy link
Copy Markdown
Contributor

Nice! 👍🏻

@calebdw calebdw force-pushed the calebdw/push-sxosyvyswkqo branch from e484dc0 to 6540965 Compare February 24, 2026 15:23
@calebdw calebdw marked this pull request as ready for review February 24, 2026 15:29
@calebdw

calebdw commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

@taylorotwell, I've updated to report the QueryException, with the option of disabling reporting

…ute binding

When implicit route model binding encounters a QueryException (e.g., from
integer overflow or type mismatch), convert it to a ModelNotFoundException
instead of letting the 500 propagate. This ensures invalid route parameters
like '/users/99999999999999999999' return a 404 rather than a 500.
@calebdw calebdw force-pushed the calebdw/push-sxosyvyswkqo branch from 6540965 to d6e5406 Compare February 24, 2026 15:35
@taylorotwell

Copy link
Copy Markdown
Member

Hmm, let me think about this one - I'm not confident we're totally landing on the right solution.

@taylorotwell taylorotwell marked this pull request as draft February 25, 2026 19:36
@calebdw

calebdw commented Mar 19, 2026

Copy link
Copy Markdown
Contributor Author

Hey @taylorotwell, just wanted to check in here---this bit us again today and it would be really nice if these could just return a 404 in prod without alerting all the devs about an exception

@calebdw

calebdw commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

Hey @taylorotwell, this bit us again today---any thoughts on what the right solution would be so we don't have to litter ->whereNumber() on every single route definition or have a ton of Route::pattern calls?

@GrahamCampbell

GrahamCampbell commented May 14, 2026

Copy link
Copy Markdown
Collaborator

For what it's worth, I feel like this is a bad idea. It's actually extremely expensive and dangerous to allow a string to query an int column in MySQL as it may will cause a full table scan and not use the index. Proper validation should be applied before the query is made.

@calebdw

calebdw commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

@GrahamCampbell, do you have any other ideas? I feel like there's bound to be a better global solution than:

  • adding ->whereNumber to hundreds (if not thousands) of routes
  • or adding Route::pattern() calls to a service provider (a place that's far removed from the actual route definitions) for every unique route parameter name (we have 300+ unique models)

Also, it's not just about non-numbers but also numbers that are too large (see above) for the primary key size.

It's actually extremely expensive and dangerous to allow a string to query an int column in MySQL as it may will cause a full table scan and not use the index.

  • it's important to note that I'm not introducing "expensive and dangerous" queries---I'm just turning exceptions from existing queries into a 404 because: 1) there's nothing to fix, 2) the user SHOULD receive a 404 instead of a 500 error page, and 3) I don't want to get pinged from an exception
  • from my understanding (and I double checked with Grok, but I suppose we both could be wrong), this is not actually a valid concern---MySQL performs implicit type coercion before comparison:
    • '123' gets converted to 123
    • '123acb' gets truncated to 123
    • 'foo' gets converted to 0

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.

4 participants