+1

Maintenance Mode cho API: Đừng để Mobile App bị Crash vì trang HTML 503!

Khi cần bảo trì server, phản xạ của 99% anh em dev là SSH vào server và gõ lệnh php artisan down. Xong đi ngủ!

Hậu quả là gì? Lệnh này mặc định sẽ trả về một trang web HTML (mã 503). Nếu hệ thống của bạn là một API phục vụ cho Mobile App (iOS/Android) hoặc Frontend React/Vue, khi chúng nhận được một mớ thẻ HTML thay vì một chuỗi JSON quen thuộc, App sẽ bị Crash (văng app) ngay lập tức! Trải nghiệm người dùng cực kỳ tồi tệ.

Hơn nữa, khi hệ thống đang "Down", làm sao đội QA (Tester) hoặc Admin có thể vào test thử xem tính năng mới đã chạy ổn chưa trước khi mở lại cho khách dùng? Lệnh mặc định của Laravel dùng Cookie để bypass, thứ rất rườm rà khi test qua API.

Hôm nay, mình sẽ hướng dẫn bạn tự build một Hệ thống Maintenance Mode bằng Cache (Redis/File) dành riêng cho API. Hệ thống này cho phép bật/tắt bảo trì không cần gõ lệnh Terminal, trả về JSON chuẩn xác, và có cơ chế Bypass Header cho dev test!

Lời mở đầu: Kiến trúc Bảo trì Không trạng thái (Stateless Maintenance)

Thay vì dùng file vật lý của Laravel, chúng ta sẽ lưu trạng thái bảo trì trên RAM (Cache). Điều này mang lại 2 lợi ích cốt lõi cho dự án Enterprise:

  1. Dễ dàng điều khiển từ xa: Bạn có thể làm một nút Bật/Tắt bảo trì ngay trên trang Dashboard Admin. Khỏi cần nhờ DevOps SSH vào server.
  2. Kiểm soát Bypass linh hoạt: Dev hoặc QA chỉ cần nhét thêm 1 cái Header bí mật (VD: X-Bypass-Token) vào Postman hoặc App Test là có thể đi xuyên qua lớp bảo trì để làm việc như bình thường.

Bắt tay vào làm ngay nào!

Bước 1: Khởi tạo dự án & Định nghĩa "Hộp đen" Cache Tạo dự án mới:

laravel new enterprise-maintenance
cd enterprise-maintenance

Chúng ta sẽ sử dụng Cache để lưu 2 thông số:

  • is_maintenance: Hệ thống có đang bảo trì không?
  • maintenance_message: Lời nhắn gửi đến user (VD: "Đang nâng cấp server, quay lại lúc 3h sáng nhé").

Bước 2: Tạo Middleware "Khiên chắn" bảo trì

Tất cả các API của hệ thống sẽ phải đi qua lớp khiên này trước khi chạm vào Controller.

php artisan make:middleware ApiMaintenanceMiddleware

Mở file vừa tạo lên và định nghĩa logic "Chặn cửa" và "Mở cửa bí mật":

// app/Http/Middleware/ApiMaintenanceMiddleware.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class ApiMaintenanceMiddleware
{
    // Cấu hình một Token bí mật để bypass. Thực tế bạn nên bỏ vào file .env
    private const BYPASS_TOKEN = 'super-secret-developer-token-2026';

    public function handle(Request $request, Closure $next)
    {
        // 1. Kiểm tra xem có Token Bypass trong Header không?
        // Nếu Dev/QA gửi đúng token bí mật, cho đi qua luôn, không cần quan tâm server có bảo trì hay không.
        if ($request->header('X-Bypass-Token') === self::BYPASS_TOKEN) {
            return $next($request);
        }

        // 2. Kiểm tra trạng thái bảo trì trong Cache
        $isMaintenance = Cache::get('system_maintenance_mode', false);

        if ($isMaintenance) {
            // Lấy lời nhắn từ Admin, nếu không có thì dùng câu mặc định
            $message = Cache::get('system_maintenance_message', 'Hệ thống đang được nâng cấp bảo trì. Vui lòng quay lại sau.');

            // 3. Đánh chặn và trả về chuẩn JSON với mã lỗi 503 (Service Unavailable)
            return response()->json([
                'success' => false,
                'error_code' => 'MAINTENANCE_MODE',
                'message' => $message,
                'estimated_uptime' => Cache::get('system_maintenance_uptime', 'Sắp xong rồi...')
            ], 503);
        }

        // Không bảo trì -> Chạy bình thường
        return $next($request);
    }
}

Lưu ý cho Laravel 11: Bạn cần đăng ký Middleware này chạy cho toàn bộ nhánh API. Mở bootstrap/app.php lên:

// bootstrap/app.php
use App\Http\Middleware\ApiMaintenanceMiddleware;

->withMiddleware(function (Middleware $middleware) {
    // Nhét khiên chắn vào nhóm API
    $middleware->api(prepend: [
        ApiMaintenanceMiddleware::class,
    ]);
})

Bước 3: Tạo Controller cho Admin điều khiển từ xa

Giờ ta viết một API ẩn dành riêng cho Admin để bật/tắt chế độ này.

php artisan make:controller Api/SystemController
// app/Http/Controllers/Api/SystemController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class SystemController extends Controller
{
    /**
     * BẬT chế độ bảo trì (Admin gọi)
     */
    public function enableMaintenance(Request $request)
    {
        // Lưu vào Cache vĩnh viễn cho đến khi bị tắt
        Cache::forever('system_maintenance_mode', true);
        
        // Lưu các thông số tùy chọn từ request
        if ($request->has('message')) {
            Cache::forever('system_maintenance_message', $request->message);
        }
        if ($request->has('estimated_uptime')) {
            Cache::forever('system_maintenance_uptime', $request->estimated_uptime);
        }

        return response()->json([
            'success' => true,
            'message' => 'Đã KÍCH HOẠT chế độ bảo trì toàn hệ thống!'
        ]);
    }

    /**
     * TẮT chế độ bảo trì
     */
    public function disableMaintenance()
    {
        Cache::forget('system_maintenance_mode');
        Cache::forget('system_maintenance_message');
        Cache::forget('system_maintenance_uptime');

        return response()->json([
            'success' => true,
            'message' => 'Đã TẮT chế độ bảo trì. Hệ thống hoạt động bình thường.'
        ]);
    }

    /**
     * API giả lập lấy danh sách sản phẩm (Để test khi bị bảo trì)
     */
    public function getProducts()
    {
        return response()->json([
            'success' => true,
            'data' => [
                ['id' => 1, 'name' => 'MacBook Pro M3'],
                ['id' => 2, 'name' => 'iPhone 17 Pro Max']
            ]
        ]);
    }
}

Mở routes/api.php đăng ký Route:

use App\Http\Controllers\Api\SystemController;

// Các API thông thường của hệ thống (User gọi)
Route::get('/products', [SystemController::class, 'getProducts']);

// Các API quản trị (Thực tế nên bọc trong Middleware xác thực Admin)
Route::post('/admin/maintenance/enable', [SystemController::class, 'enableMaintenance']);
Route::post('/admin/maintenance/disable', [SystemController::class, 'disableMaintenance']);

Bước 4: Thử lửa Postman - Trải nghiệm sự chuyên nghiệp

Bật server: php artisan serve

Kịch bản 1: Hệ thống hoạt động bình thường (User gọi API) Method: GET

URL: http://127.0.0.1:8000/api/products

Kết quả: Nhả JSON 200 OK bình thường. App chạy ngon lành.

Kịch bản 2: Nửa đêm, Admin bật khiên bảo trì Method: POST

URL: http://127.0.0.1:8000/api/admin/maintenance/enable

Body (JSON):

{
    "message": "Hệ thống đang update Database để chạy Flash Sale.",
    "estimated_uptime": "Dự kiến hoàn thành lúc 03:00 AM."
}

Kết quả: Chế độ bảo trì đã bật.

Kịch bản 3: User đang thức đêm lướt App gọi API Method: GET

URL: http://127.0.0.1:8000/api/products

Kết quả (Cực kỳ an toàn cho App Mobile):

{
    "success": false,
    "error_code": "MAINTENANCE_MODE",
    "message": "Hệ thống đang update Database để chạy Flash Sale.",
    "estimated_uptime": "Dự kiến hoàn thành lúc 03:00 AM."
}

Frontend nhận được mã HTTP 503 và chuỗi JSON chuẩn. Nó sẽ hiện ra một cái Popup xinh xắn trên màn hình điện thoại thay vì bị Crash tung tóe!

Kịch bản 4: Dev/QA đi xuyên tường để test hệ thống Hệ thống đang bảo trì, nhưng sếp bảo em test lại xem code mới chạy chưa. Làm sao đây?

Method: GET

URL: http://127.0.0.1:8000/api/products

Headers: Thêm khóa X-Bypass-Token với giá trị super-secret-developer-token-2026.

Kết quả: Nhả JSON danh sách sản phẩm (200 OK)! Bạn hoàn toàn tự do tung hoành trong hệ thống để test tính năng mới, trong khi khách hàng ngoài kia vẫn đang nhìn thấy bảng thông báo bảo trì.

Kịch bản 5: Test xong, Admin mở lại hệ thống Method: POST

URL: http://127.0.0.1:8000/api/admin/maintenance/disable

Kết quả: Mọi giới hạn được gỡ bỏ. Tất cả user gọi lại GET /api/products đều thành công.

Tóm lại

Chỉ với một lớp Middleware mỏng nhẹ và Cache, chúng ta đã giải quyết được 3 bài toán lớn của cấp độ Enterprise:

Không gây Crash App: Luôn trả về định dạng JSON thay vì HTML thô bạo.

Không chạm Terminal: Tích hợp Bật/Tắt thẳng vào CMS Admin Panel cho các sếp quản lý vận hành tự xử lý.

Bảo mật & Linh hoạt: Cơ chế Bypass bằng Header cho phép Dev Team thoải mái kiểm thử trên Production ngay cả khi hệ thống đóng cửa.

Đó là cách làm việc của một Kỹ sư hệ thống thực thụ! Lôi ngay con code này ốp vào dự án của anh em nhé.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí