t0berius's avatar

maintenance mode (excluding some routes)

I know laravel comes with a maintenance mode. I have my admin panel (routes start with /admin). Now I would like to have a button in my admin panel to set the whole laravel app into maintenance mode, so visitors should see a static HTML site but I would like to be able to work in the admin panel while app is in maintenance mode. -All hints/ideas welcome!

How can I achieve this best?

0 likes
26 replies
skliche's avatar

@jaheller You could write your own middleware based on CheckForMaintenanceMode and work with an exclusion list like the VerifyCsrfToken middleware. But keep in mind that you either need to exclude the login route or provide a special admin login route otherwise you won't be able to login.
In order to enable this middleware permanently put it into kernel.php's middleware array.

t0berius's avatar

@skliche Admin login would be at /admin/login. Excluding /admin/* should do this. Could you tell me more about your idea (middleware, where to store the "status" of maintenance...). I tought the same, using the default maintenance mode would be to hard to modify.

skliche's avatar

@jaheller Base your middleware on the exact same code as CheckForMaintenanceMode and therefor utilize the same method (file /storage/framework/down). Replace the line \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, in kernel.php with your own middleware.

protected $except = [
        // URIs, https://laravel.com/docs/master/routing#csrf-excluding-uris
    'admin/*',
];

public function handle($request, Closure $next) {
    if ($this->app->isDownForMaintenance() && ! $this->shouldPassThrough($request)) {
        throw new HttpException(503);
    }

    return $next($request);
}

shouldPassThrough() would check the current route against the exempted routes. See shouldPassThrough() in /vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php. You should be able to use the exact same code.

t0berius's avatar

@skliche

The steps I understand how to do.

-Create before middleware in app/Http/Middleware directory (code above should do the job)

-Replace the line \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, in kernel.php

That's all? No need to "attach" middleware to each controller like this right? Because it runs without being attached to a controller?

public function __construct()
{
    $this->middleware(['whatever_the_name_may_be_of_middleware?','auth']);
}
skliche's avatar

@jaheller Yes, as long as you replace the existing line in kernel.php with your middleware there is no need to call it in a controller or specify it on a route / route group as this is the global middleware stack. Therefor it will be called for every single request to your application.

t0berius's avatar

@skliche

Sorry for late answer, now I'm on this. Error saying

Undefined property: App\Http\Middleware\MaintenanceMiddleware::$app

It's the line with the if statement. Did I need to include something before? my MaintenanceMiddleware:

<?php
namespace App\Http\Middleware;
use Closure;

class MaintenanceMiddleware
{
    protected $except = 
    [
    // URIs, https://laravel.com/docs/master/routing#csrf-excluding-uris
    'admin/*',
    ];

    public function handle($request, Closure $next) 
    {
    if ($this->app->isDownForMaintenance() && ! $this->shouldPassThrough($request)) 
    {
            throw new HttpException(503);
    }
    return $next($request);
    }
}
amaya007's avatar

Create before middleware in app/Http/Middleware directory and you replace the living line in kernel.php with your middleware there is no need to call it in a controller . That's all i know.

t0berius's avatar

Works great. Sad to see such a great pull request was turned down. Specially working with payment processors like paypal this will be the best solution to be able to exclude IPs and routes at the same time. Sorry for you, but you've done great work. Works fine :). At least one happy user :/.

lee__mason's avatar

great to hear it, yes my argument was callbacks for payments and admin usage.

maybe it will get there some day, in the meantime its not too tiresome to implement.

t0berius's avatar

@no12pixels Did you have any idea how I can easily turn the laravel app into maintenance mode and back to live by just using a button in my admin panel? So changing the status of the app without using "command line (artisan)"?

lee__mason's avatar

@jaheller should be pretty simple, just use the artisan facade:

Artisan::call('up')

performs the same task as console artisan commands, its in the docs here: https://laravel.com/docs/5.2/artisan#calling-commands-via-code

you would use this command inside a controller method by checking if the button is pressed when the forms submitted. eg:

public function postMode(Request $request){
    if($request->input('enabled_maintenance')){
        Artisan::call('down');
        return ....;
    }
    if($request->input('disable_maintenance')){
        Artisan::call('up');
        return ....;
    }
}
1 like
DucTonyVN's avatar

@lee__mason I'm following your example, but it doesn't seem to be getting anything can you give the parts you do about it

Snapey's avatar

@DucTonyVN don't follow advice in this question. maintenance mode has been improved in the last 5 years

1 like
DucTonyVN's avatar

@Snapey The real problem here is that I have to create a button to click on maintenance I can't use php artisan down and up for maintenance

t0berius's avatar

@no12pixels

Today I noticed the exclude of routes seems to be not working. I use this to exclude route:

protected $excludedRoutes = ['admin/*' , '/stats'];

Using the exclude IP adresses works fine. my routes:

Route::get('admin', 'DashboardController@admin');
lee__mason's avatar

@jaheller its not so much thats its now working, more that its not designed to work with wildcards.

the $excludedRoutes property takes in an array of concrete route names, not wildcard route names, maybe something i wrap up in a package with additional functionality.

t0berius's avatar

@no12pixels I tried to use:

protected $excludedRoutes = ['stats', 'admin'];

route:

//stats routes
Route::get('stats', 'statsController@index');

doesn't work too. Exclude IPs works fine. Where's the fault? //admin panel Route::get('admin', 'DashboardController@admin');

lee__mason's avatar

you need to give the route a name, its not a url matcher, its a route name matcher, your route should look like this:

Route::get('stats', ['as' => 'stats', 'uses' => 'statsController@index']);
t0berius's avatar

@skliche @no12pixels

I modified no12pixels code a bit to something like this:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\Route;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Original;

class CheckForMaintenanceMode extends Original
{

    protected $excludedNames = [];

    protected $except = ['admin/*', 'stats'];

    protected $excludedIPs = [];

    protected function shouldPassThrough($request)
    {
        foreach ($this->except as $except) {
            if ($except !== '/') {
                $except = trim($except, '/');
            }

            if ($request->is($except)) {
                return true;
            }
        }

        return false;
    }



    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     *
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
     */
    public function handle($request, Closure $next)
    {
        if ($this->app->isDownForMaintenance()) {
            $response = $next($request);

            if (in_array($request->ip(), $this->excludedIPs)) {
                return $response;
            }

            $route = $request->route();

            if ($route instanceof Route) {
                if (in_array($route->getName(), $this->excludedNames)) {
                    return $response;
                }
            }

            if ($this->shouldPassThrough($request)) 
            {
                dd("YA");
                return $response;
            }

            throw new HttpException(503);
        }

        return $next($request);
    }
}

Any idea why I get a maintenance page when I will /admin? When I use for example /admin/random it works fine. How to get it working by route "/admin" too?

1 like
skliche's avatar

@jaheller Your current code needs a trailing slash after admin, try protected $except = ['admin*', 'stats'];

t0berius's avatar

@skliche Works, but is there an easy way to get the rule just match on "exact" route. For example:

/adminHAHSAH/ndsanndsa ->valid, but only /admin and /admin/herecomeroutes should be valid /admin/dhsah->valid correct!

At the moment it seems like it just checks if the route contains "admin", if this returns true the whole route is valid, that'why why something like "adminBLA/xyz" is valid too.

Lina's avatar

How can you change the expluded ips?

hen can i put this array to access from anyside of my code like a global variable?

Please or to participate in this conversation.