Redirect in v4 struggle

I have invoked a class through $app->post() from routes.php

And the script runs well without errors

public function __invoke(Request $request, Response $response): Response {
    // your code
    // to access items in the container... $this->container->get('');
        return $response->withHeader('Location', '/')
            ->withStatus(201);
}

And i see on Console that the Location and Status is actually correctly set, but the browser wont redirect me. Why is this?

Because you’re using a 201 Created status. If you’re redirecting after a POST, you probably want a 303 See Other

The Location response header indicates the URL to redirect a page to. It only provides a meaning when served with a 3xx (redirection) or 201 (created) status response.

This is from Mozilla-dev, do i misunderstand this one? Shouldnt it redirect if i send a 201 created? (Location - HTTP | MDN)

If you return a location on a 201, that’s telling the client where they can find the resource. It doesn’t redirect. The only way to redirect is to use a 3xx status. They’re literally defined by the W3C as:

  • 1xx: Informational
  • 2xx: Successful
  • 3xx: Redirection
  • 4xx: Client Error
  • 5xx: Server Error

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Ill try that, i have another issue atm.

Exact same struggle here. Redirects work fine for me in normal classes, but I cannot seem to get them working in middleware, where a response is dynamically generated (apparently?) by the Request Handler. When I try to redirect with a Location header, it simply fails to redirect, and my route continues to the original location.

Here’s a basic version of my authentication middleware:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class AuthMiddleware extends Middleware {

	public function __invoke(Request $request, RequestHandler $handler): Response {
		$response = $handler->handle($request);
		$loggedInTest = false;
		if ($loggedInTest) {
			echo "User authorized.";
			return $response;
		} else {
			echo "User NOT authorized.";
			return $response->withHeader('Location', '/users/login')->withStatus(302);
		}
	}
}

Thank you in advance.

Hi, havent tested it but maybe try this…


class AuthMiddleware extends Middleware {

	public function __invoke(Request $request, RequestHandler $handler): Response {
		$loggedInTest = false;
		if ($loggedInTest) {
			echo "User authorized.";
			return $handler->handle($request);;
		} else {
			echo "User NOT authorized.";

                      $response = new Response();
			return $response->withHeader('Location', '/users/login')->withStatus(302);
		}
	}
}


Hmm, still a no-go, unfortunately. Browser simply displays the “Not Authorized” text, then hangs on a white screen without actually redirecting…

Chris

A HTTP header can only be sent before sending a body content. I would guess that the echo statements are problematic here.

Very good point. Unfortunately, it still fails to redirect. It falls thru to the originally called route, completely ignoring the redirect header. If I initiate a new Response, as suggested by FvsJson, it hangs on a white screen.

Stumped.

Okay, I figured it out. Basically, I’m an idiot. My code was stuck in a perpetual redirect loop. I had to use the $_SERVER[‘REQUEST_URI’] variable to break out of the loop, like so:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class AuthMiddleware extends Middleware {

    public function __invoke(Request $request, RequestHandler $handler): Response {
        $response = $handler->handle($request);
        $loggedInTest = false;
        if (!$loggedInTest && $_SERVER['REQUEST_URI'] != '/user/login') {
            return return $response->withHeader('Location', '/users/login')->withStatus(302);
        } else {
            return $response;
        }
    }
}

Does anybody have another way to accomplish this, or is the $_SERVER method the best way?

Better use the $request object for all request-related things.

$uri = $request->getUri();

http://www.slimframework.com/docs/v4/objects/request.html#the-request-uri

PS: Using a fixed string here '/user/login' could be problematic in case the basePath changes later.

1 Like

And so we all learn together :stuck_out_tongue:

Ugh, I spoke too soon. Basically, I got so frustrated trying to make Slim 4’s redirect work in a middleware scenario that I went dabbling in FatFreeFramework, and found I had the same problem. That clued me in that maybe it was something I was doing that was the culprit. Long story short, in my FFF testing, I was definitely putting my test app in an infinite redirect loop, so I assumed it was the same issue here. But sadly it is not.

In my Slim 4 app, my ‘login’ method is in a completely different class, and it obviously is NOT behind authentication. And sadly, the redirect still falls thru to the original route. Anybody have an idea on this? Has anybody else made redirect work in middleware, and if so, can you post a snippet showing how?

There are some things to consider in this case.

Make sure that The AuthMiddleware will be only invoked for routes that needs the AuthMiddleware. In Slim, you could create a special Route group for all “protected” routes and create another route group for all login/logout-related routes, but without the AuthMiddleware.

If you try this concept, the AuthMiddleware only have to check for the logged in user. I would also call the handle method only for valid users. Here is an example.

Routes

use Slim\App;
use Slim\Routing\RouteCollectorProxy;

return static function (App $app) {

    // Routes without authentication check
    $app->group('/users', function (RouteCollectorProxy $group) {
        $group->post('/login', \App\Action\UserLoginSubmitAction::class);
        $group->get('/login', \App\Action\UserLoginIndexAction::class)->setName('login');
        $group->get('/logout', \App\Action\UserLogoutAction::class);
    })->add(SessionMiddleware::class);

    // Routes with authentication
    $app->group('', static function (RouteCollectorProxy $group): void {
        // Default page
        $group->get('/', \App\Action\HomeIndexAction::class)->setName('root');

        // add more routes
        // ...
    })->add(AuthMiddleware::class)
      ->add(SessionMiddleware::class);

};

The AuthMiddleware

Pseudo example:

<?php

namespace App\Middleware;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;

/**
 * Auth Middleware.
 */
final class AuthMiddleware implements MiddlewareInterface
{
    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    /**
     * Constructor.
     *
     * @param ResponseFactoryInterface $responseFactory The response factory
     */
    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    /**
     * Invoke middleware.
     *
     * @param ServerRequestInterface $request The request
     * @param RequestHandlerInterface $handler The handler
     *
     * @return ResponseInterface The response
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $isLoggedIn = !empty($_SESSION['user_id']); // check user login / session here

        if ($isLoggedIn) {
            return $handler->handle($request);
        }

        // Redirect to login route
        $routeParser = RouteContext::fromRequest($request)->getRouteParser();
        $url = $routeParser->urlFor('login');

        return $this->responseFactory->createResponse()->withHeader('Location', $url)->withStatus(302);
    }
}
1 Like

I think Odan nailed it. Your auth middleware is currently set to global so it hits a loop. you need to only assign the auth middleware to the route that needs to be authenticated.


    // Routes with authentication
    $app->group('', static function (RouteCollectorProxy $group): void {
        // Default page
        $group->get('/', \App\Action\HomeIndexAction::class)->setName('root');

        // add more routes
        // ...
    })->add(AuthMiddleware::class)
      ->add(SessionMiddleware::class);

well done Oden.

1 Like

Yes, in my testing code, it was definitely a redirect loop, but in the app I’m actually considering upgrading from Slim 3, the login method is not behind authentication, and the redirect still fails.

However, examining Odan’s code, I see he’s used the ResponseFactory to create a fresh response for the redirect, which is something I did not do and will have to try. Life has gotten in the way the past few days, so I haven’t had a chance to try it yet…

So, if I want to redirect I should create new response, but to continue just use handle method? Is that right?

So, if I want to redirect I should create new response, but to continue just use handle method? Is that right?

Yes, that’s correct.