phplaravelhttp-redirectlaravel-11

How I can prevent the redirections in API using middleware on Laravel 11?


According to this answer I need to override the default behaviour for my api calls and prevent the redirections regardless the header using a middleware:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class ApiMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        Log::info(__FUNCTION__);
        $response = $next($request);
        if ($response->getStatusCode() === 302 || $response->getStatusCode() === 301) {
            return new JsonResponse(['msg'=>"Unauthorized"], 401);
        }

        return $response;
    }
}

And upon routes/api.php using the following aproach:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

use App\Http\Middleware\ApiMiddleware;

Route::middleware([ApiMiddleware::class])->group(function (){

    // Generating the token
    Route::put('/token',[\App\Http\Controllers\API\SaasUserController::class,'login'])
        ->name('api.login');


    Route::middleware('auth:sanctum')->group(function (){
        Route::get('/user', function (Request $request) {
            return $request->user();
        });
        // Misc routes that need authentication
    });
});

But by doing a plain GET to /api/user I get redirection according to insomnia log:


> GET /api/user HTTP/1.1
> Host: 172.161.5.2
> User-Agent: insomnia/2022.6.0
> Cookie: my_session=eyJpdiI6IjBxWVJkMlJobTREYVFuMk9wZ2xnV0E9PSIsInZhbHVlIjoiRmlRbnlwSlhBbE55a0FJS21hWE9reCs0Q1FPOW03bU1vcy9zOFhrZG1QTHlDYmgwV1VqUVgyNFJpUXE3cmxrOWI4d2lFcktDVFMydmxjZ0VmcFF3WGQvOWYvM0V4dXhQb2xya1ppNEVBNnZDMXBQNUJsaDJmV3NBMTdjdmpYREEiLCJtYWMiOiI2MGQ4MDA1ZTU1MTczYjM4NDdkNjFjOTE1ZDg3ZGU4YTUwOTlkOGRhY2EyZTJlZDU2ZTFhMWVlMWIyMjAyYWZiIiwidGFnIjoiIn0%3D; XSRF-TOKEN=xxxx
> Accept: */*

* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse

< HTTP/1.1 302 Found
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/8.2.17
< Cache-Control: no-cache, private
< Date: Thu, 28 Mar 2024 10:29:17 GMT
< Location: https://172.161.5.2/login
< Access-Control-Allow-Origin: *


* Ignoring the response-body
* Received 358 B chunk
* Connection #6 to host 172.161.5.2 left intact
* Issue another request to this URL: 'https://172.161.5.2/login'
* Found bundle for host 172.161.5.2: 0x3f5c038f58c0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#6) with host 172.161.5.2
* Connected to 172.161.5.2 (172.161.5.2) port 443 (#6)

> GET /login HTTP/1.1
> Host: 172.161.5.2
> User-Agent: insomnia/2022.6.0
> Cookie: my_session=eyJpdiI6IjBxWVJkMlJobTREYVFuMk9wZ2xnV0E9PSIsInZhbHVlIjoiRmlRbnlwSlhBbE55a0FJS21hWE9reCs0Q1FPOW03bU1vcy9zOFhrZG1QTHlDYmgwV1VqUVgyNFJpUXE3cmxrOWI4d2lFcktDVFMydmxjZ0VmcFF3WGQvOWYvM0V4dXhQb2xya1ppNEVBNnZDMXBQNUJsaDJmV3NBMTdjdmpYREEiLCJtYWMiOiI2MGQ4MDA1ZTU1MTczYjM4NDdkNjFjOTE1ZDg3ZGU4YTUwOTlkOGRhY2EyZTJlZDU2ZTFhMWVlMWIyMjAyYWZiIiwidGFnIjoiIn0%3D; XSRF-TOKEN=XXXXXX
> Accept: */*

* Mark bundle as not supporting multiuse

< HTTP/1.1 200 OK
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/8.2.17
< Cache-Control: no-cache, private
< Date: Thu, 28 Mar 2024 10:29:17 GMT

* Replaced cookie XSRF-TOKEN="XXXXXX" for domain 172.161.5.2, path /, expire 1711628957

< Set-Cookie: XSRF-TOKEN=XXXXXX; expires=Thu, 28 Mar 2024 12:29:17 GMT; Max-Age=7200; path=/; secure; samesite=lax

* Replaced cookie my_session="eyJpdiI6InBXcHQrVnc1QVVVbVBpckd1eE5VS0E9PSIsInZhbHVlIjoidEF5RGlHRTIvVFdGNnBodVdMWC9UYWYydUVzanJFOTRBcjN3WTZuYUgvSHpFempNWUZiZjVHSGJCandHdUZFakNxRFpwbGo1WGxYVWpnSjA3VlF1ZnZKZENtUWFJUENwMW9EMyt6UmpidjZWVzZTdkIrekk4d24xK0R4Wi9IeloiLCJtYWMiOiI3MzE3YmU3OGY1MTc0NTJmYjVlZTZkMDNiOWE1YTkwNGFiNGMyNmNiOWUwMThmNTFkNDg3ZWVkNGMyNGIyOGNmIiwidGFnIjoiIn0%3D" for domain 172.161.5.2, path /, expire 1711628957

< Set-Cookie: my_session=eyJpdiI6InBXcHQrVnc1QVVVbVBpckd1eE5VS0E9PSIsInZhbHVlIjoidEF5RGlHRTIvVFdGNnBodVdMWC9UYWYydUVzanJFOTRBcjN3WTZuYUgvSHpFempNWUZiZjVHSGJCandHdUZFakNxRFpwbGo1WGxYVWpnSjA3VlF1ZnZKZENtUWFJUENwMW9EMyt6UmpidjZWVzZTdkIrekk4d24xK0R4Wi9IeloiLCJtYWMiOiI3MzE3YmU3OGY1MTc0NTJmYjVlZTZkMDNiOWE1YTkwNGFiNGMyNmNiOWUwMThmNTFkNDg3ZWVkNGMyNGIyOGNmIiwidGFnIjoiIn0%3D; expires=Thu, 28 Mar 2024 12:29:17 GMT; Max-Age=7200; path=/; httponly; samesite=lax


Is there a way to override this without needing to provide accept header?


Solution

  • In order to achieve this you need to apply the middleware globally at bootstrap/app.php like this:

    use Illuminate\Foundation\Application;
    use Illuminate\Foundation\Configuration\Exceptions;
    use Illuminate\Foundation\Configuration\Middleware;
    
    return Application::configure(basePath: dirname(__DIR__))
        ->withRouting(
            web: __DIR__.'/../routes/web.php',
            api: __DIR__.'/../routes/api.php',
            commands: __DIR__.'/../routes/console.php',
            health: '/up',
        )
        ->withMiddleware(function (Middleware $middleware) {
            $middleware->api(prepend: [
                \App\Http\Middleware\ApiMiddleware::class
            ]);
            // Rest of middleware bootstrapping goes here
        })
        ->withExceptions(function (Exceptions $exceptions) {
           // Code ommited here if any
        })->create();
    

    Then you can modify the middleware using these options as api stategies:

    option 1 leave it as is

    Keep the middleware as you have

    option 2

    Send a 401 response in a format that the server accepts as well:

    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\JsonResponse;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Log;
    use Symfony\Component\HttpFoundation\Response;
    
    class ApiMiddleware
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
         * @return Response
         */
        public function handle(Request $request, Closure $next): Response
        {
            $response = $next($request);
    
            if ($response->getStatusCode() === 302 || $response->getStatusCode() === 301) {
    
               $accept=$request->header('Accept')??"";
               $accept=trim(preg_replace(";.*","",$accept));
               if(!empty($accept) && $accept!='application/json'){
                        $content="Unautorized";
    
                        switch ($accept){
                            case "application/html":
                            case "text/html":
                                $content="<!DOCTYPE html><html><head><tilte>Unautorized</tilte></head><body>Unautorized</body></html>";
                                break;
                            case 'application/xml':
                            case 'text/xml':
                                $content="<xml><msg>Unautorized</msg></xml>";
                                break;
                            default:
                                $accept="text/plain";
                        }
                       return new Response($content,401,['Content-Type',$accept]);
                }
    
    
                return new JsonResponse(['msg'=>"Unauthorized"], 401);
            }
    
            return $response;
        }
    }
    
    
    

    Option 3 Let the client to provide you an accept header as application/json

    class ApiMiddleware
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
         * @return Response
         */
        public function handle(Request $request, Closure $next): Response
        {
            if(!$request->wantsJson()){
              new Response("",400);
            }
    
            $response = $next($request);
            return $response;
        }
    }
    

    Option 4

    As mentioned fill the necessary header as shown in https://laraveldaily.com/tip/force-json-response-for-api-requests

    Option 5 A combination of #option3 and #option4

    That enforces that we only api accepts only Json.

    class ApiMiddleware
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
         * @return Response
         */
        public function handle(Request $request, Closure $next): Response
        {
            if(!$request->wantsJson()){
              new JsonResponse(['msg'=>"Invalid provided Accept Type"],400);
            }
    
            $request->headers->set('Accept', 'application/json');
    
            $response = $next($request);
            return $response;
        }
    }