The Wayback Machine - https://web.archive.org/web/20230619161710/https://github.com/laravel/framework/pull/47317
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

[11.x] Improve API error handling #47317

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft

Conversation

tontonsb
Copy link
Contributor

@tontonsb tontonsb commented Jun 2, 2023

I've come across two common issues when working with API backends and testing the endpoints in browser or just not knowing to specify the Accept: application/json header. Most projects work around this by adding some middleware that either adds the header to all incoming API requests or sets a forceJson flag on their custom request.

This PR proposes to remedy these issues by taking a "only redirect if there's a reasonable redirect target" approach.

Unauthenticated user

When hitting an endpoint that requires authentication, but failing the auth, user should receive an authentication error. Instead they receive a 500 error with Route [login] not defined. message.

Proposed solution: only redirect to route('login') if the route exists. Otherwise fall back to JSON response.

Backwards compatibility? Technically it is a breaking change, but I can't imagine a legit scenario where someone would rely on their auth errors being redirected to non-existant route, catching the particular error and doing some useful reporting with it.

Validation error

When getting some request params wrong, users should see validation errors. Instead users that are hitting the endpoint in their browserse receive a redirect to /. Because no "previous" route exists and even if it did, Laravel wouldn't know it as the session is not enabled on API routes.

Proposed solution: only redirect to "previous" route if it exists in the session. Do not use url()->previous() as

  • falling back to url('/') for all validation errors is unlikely to be useful in real usecases (although common in automated tests)
  • redirecting to referrer would be useless as they wouldn't receive the error bag or error code in any way

Backwards compatibility?

  1. This will require to update web (non-json) validation tests that expect a 302 redirect now. Two options:
    • Expect 422 instead of 302, i.e. change assertRedirect or assertStatus(302) to assertInvalid or assertStatus(422).
    • Set up the session with the previous URL as it would be in a real life workflow. Add session()->setPreviousUrl($prevUrl); to the test before hitting the endpoint that should redirect you back.
  2. This would also break the workflow if someone has set their index page as a dedicated error displayer. I don't know if someone actually does that. There are multiple ways to restore the previous behaviour, e.g. overriding the handler's invalid method or catching the exceptions in handler (and maybe just specifying ->redirectTo('/') on them which is still respected).
@tontonsb tontonsb marked this pull request as draft June 2, 2023 06:47
@tontonsb tontonsb changed the title Improve API error handling [11.x] Improve API error handling Jun 2, 2023
@ankurk91
Copy link
Contributor

ankurk91 commented Jun 2, 2023

I faced the same issue recently, since my app did not have any route named login.

Laravel core must not auto assume that the this route exists in Skelton

@tontonsb tontonsb marked this pull request as ready for review June 2, 2023 20:04
Comment on lines +170 to +176
Route::get('test-route', fn () => throw new AuthenticationException);

$this->assertFalse(Route::has('login'));

$this->get('test-route')
->assertStatus(401)
->assertSeeText('Unauthenticated.');
Copy link
Contributor

Choose a reason for hiding this comment

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

When would an application not have a login route, but yet still have routes protected by auth that would throw an authentication exception like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When you're using any other auth mechanism, e.g. token in the headers or query.

Comment on lines +493 to +495
return $this->shouldReturnJson($request, $exception) || ! $redirectTo
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest($exception->redirectTo() ?? route('login'));
: redirect()->guest($redirectTo);
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong, but with this logic it is currently possible for a route to return a JSON response when it's not expected to, if there is no $redirectTo target. Is this correct behaviour?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's exactly the point of this change.

I am arguing that receiving 500 | Server error or Route [login] not defined. is wrong (and is not useful) if the actual issue is that you're not authenticated and returning the JSON message is the most reasonable fallback in that case.

@taylorotwell taylorotwell marked this pull request as draft June 7, 2023 14:08
@taylorotwell
Copy link
Member

Going to revisit this a bit later. I'm currently doing some heavy work on the skeleton, exception handler, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants