Khắc phục lỗi Nginx 'Internal Redirection Cycle' (Vòng lặp try_files)

intermediate Nginx2026-07-12| Nginx 1.18+, Ubuntu 22.04/24.04, Debian 12, Single Page Applications (React, Vue, Vite), PHP 8.2/8.3 FPM.

Error Message

rewrite or internal redirection cycle while processing
#nginx#devops#cấu hình server#xử lý sự cố

Vấn đề

Bạn vừa hoàn tất việc triển khai một Single Page Application (SPA) hoặc một dự án Laravel. Mọi thứ dường như hoàn hảo cho đến khi bạn tải lại trang và gặp lỗi 500 Internal Server Error. Khi kiểm tra nhật ký lỗi (error logs) của Nginx, bạn thấy dòng thông báo gây đau đầu này:

rewrite or internal redirection cycle while processing "/index.html"

Điều này xảy ra do Nginx bị kẹt trong một vòng lặp. Nó tìm kiếm một tệp tin, không thấy, nhảy sang phương án dự phòng (fallback), và phương án dự phòng đó lại kích hoạt chính logic tìm kiếm tương tự. Nginx có giới hạn cứng là 10 lần chuyển hướng nội bộ. Một khi chạm đến lần thử thứ 11, nó sẽ hủy yêu cầu để tránh làm quá tải CPU của bạn.

Môi trường

  • Hệ điều hành: Các bản phân phối Linux hiện đại (Ubuntu 22.04+, Debian, RHEL)
  • Phần mềm: Nginx (mọi phiên bản)
  • Các tình huống phổ biến: Định tuyến phía client (client-side routing) trong React/Vue, định tuyến index.php trong các PHP framework, hoặc các trình xử lý lỗi 404 tùy chỉnh.

Vòng lặp bắt đầu như thế nào

Thông thường, rắc rối bắt đầu từ một chỉ thị try_files quá "lạc quan". Hãy xem cấu hình phổ biến (nhưng có lỗi) dành cho một ứng dụng React dựa trên Vite sau đây:

server {
    listen 80;
    root /var/www/my-app/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Đoạn mã này yêu cầu Nginx kiểm tra tệp tin thực tế, sau đó là thư mục. Nếu cả hai đều không tìm thấy, nó sẽ trả về index.html.

Cái bẫy rất đơn giản: Nếu index.html thực sự bị thiếu trong thư mục /dist (có lẽ do quá trình build bị lỗi), Nginx không dừng lại. Nó chuyển hướng đến /index.html, điều này lại kích hoạt khối location / một lần nữa. Nó tìm kiếm, thất bại và lại chuyển hướng đến /index.html lần thứ hai. Quá trình này lặp lại cho đến khi Nginx chạm giới hạn 10 chu kỳ và bỏ cuộc.

Các bước gỡ lỗi (Debugging)

1. Kiểm tra nhật ký lỗi

Xác định đường dẫn nào đang bị lỗi bằng cách theo dõi nhật ký trong thời gian thực. Hãy chạy lệnh này trong khi tải lại trình duyệt của bạn:

tail -f /var/log/nginx/error.log

Hãy chú ý kỹ đến đường dẫn trong lỗi. Nếu nó ghi là while processing "/index.php", Nginx đang báo cho bạn biết rằng index.php là mắt xích bị thiếu trong chuỗi xử lý.

2. Xác minh đường dẫn tệp và quyền truy cập

Kiểm tra xem tệp dự phòng có thực sự tồn tại ở nơi bạn nghĩ hay không. Chạy nhanh lệnh ls trên đường dẫn gốc của bạn:

ls -la /var/www/my-app/dist/index.html

Nếu tệp tin đã ở đó, Nginx vẫn có thể bị chặn không cho đọc. Sử dụng namei để kiểm tra toàn bộ chuỗi quyền truy cập. Mọi thư mục từ / xuống đến tệp tin của bạn phải có quyền thực thi (+x) cho người dùng www-data:

namei -om /var/www/my-app/dist/index.html

3. Sử dụng Named Locations để an toàn hơn

Named locations (vị trí được đặt tên) là một giải pháp thay thế sạch sẽ hơn. Chúng tách biệt logic kiểm tra tệp khỏi việc thực thi dự phòng thực tế, ngăn chặn tình trạng đệ quy vô tình. Đây là cách tiêu chuẩn trong ngành để xử lý các SPA.

Dành cho SPA (React/Vue/Vite):

location / {
    try_files $uri $uri/ @fallback;
}

location @fallback {
    rewrite ^ /index.html break;
}

Dành cho PHP (Laravel/WordPress):

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

Trong các thiết lập PHP, vòng lặp thường xảy ra bên trong khối .php. Để dừng chu kỳ, hãy thêm =404 vào chỉ thị try_files bên trong trình xử lý PHP của bạn. Điều này đảm bảo rằng nếu index.php không tồn tại, Nginx sẽ trả về lỗi 404 thay vì lặp lại.

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Giải pháp cuối cùng: Cấu hình mạnh mẽ

Nếu bạn thích thiết lập try_files đơn giản, bạn phải đảm bảo tham số cuối cùng không kích hoạt một tìm kiếm mới. Đây là cấu hình đã được thử nghiệm thực tế cho một ứng dụng web hiện đại:

server {
    listen 80;
    root /var/www/html;
    index index.html;

    location / {
        # Thử tìm tệp, sau đó là thư mục. 
        # Nếu cả hai đều thất bại, gửi yêu cầu tới index.html.
        try_files $uri $uri/ /index.html;
    }

    # Xử lý rõ ràng index.html để ngắt các vòng lặp tiềm ẩn
    location = /index.html {
        # Chỉ thị 'internal' ngăn người dùng bên ngoài gọi trực tiếp 
        # location này nếu tệp bị thiếu.
        internal;
    }
}

Xác minh

Trước khi kết thúc, hãy luôn kiểm tra cú pháp Nginx của bạn. Một dấu chấm phẩy bị thiếu có thể làm sập toàn bộ trang web.

sudo nginx -t

Nếu bạn thấy thông báo syntax is ok, hãy áp dụng các thay đổi:

sudo systemctl reload nginx

Cuối cùng, hãy kiểm tra một đường dẫn không tồn tại bằng curl. Nếu bạn nhận được 200 OK (phương án dự phòng của SPA) hoặc 404 thay vì 500, bạn đã phá vỡ vòng lặp thành công:

curl -I http://localhost/this-route-does-not-exist

Bài học rút ra

  • Dự phòng là chuyển hướng: Mục cuối cùng trong try_files là một lệnh chuyển hướng nội bộ. Nếu tệp đó bị thiếu, quá trình tìm kiếm sẽ bắt đầu lại từ đầu.
  • Lỗi đánh máy đường dẫn gốc: Kiểm tra kỹ đường dẫn root của bạn. Nếu Nginx đang tìm trong /var/www/html nhưng tệp của bạn nằm trong /var/www/dist, nó sẽ lặp vô tận.
  • Named locations an toàn hơn: Sử dụng cú pháp @name cho các định tuyến phức tạp để giữ cho logic của bạn rõ ràng.
  • Quyền truy cập rất quan trọng: Nginx không thể phục vụ những gì nó không thể đọc. Đảm bảo www-data sở hữu các tệp tin đó.

Related Error Notes