Fix PHP Warning: Cannot modify header information - headers already sent

beginner🐘 PHP2026-03-22| PHP 7.x / 8.x trên Linux (Apache, Nginx + PHP-FPM), Windows (XAMPP, WAMP)

Error Message

Warning: Cannot modify header information - headers already sent
#php#headers#output#redirect

Tình huống

2 giờ sáng. Một redirect không hoạt động. Người dùng thấy trang trắng thay vì dashboard. Log hiển thị:

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/includes/config.php:1) in /var/www/html/login.php on line 45

Thông báo đó nói lên gần như tất cả. PHP đã cố gọi header() — hoặc setcookie(), hoặc session_start() — nhưng có gì đó đã đẩy dữ liệu ra trình duyệt trước đó. Một khi output đã được gửi, HTTP header bị khóa lại. Xong.

Nguyên nhân gây ra lỗi

Nguyên nhân luôn giống nhau: có thứ gì đó tạo ra output trước khi bạn gọi header. Phần khó là "output" đó có thể gần như vô hình.

Các thủ phạm thường gặp:

  • Một dấu cách hoặc xuống dòng trước thẻ mở <?php
  • UTF-8 BOM ở đầu file (vô hình trong hầu hết các editor — 3 byte âm thầm: EF BB BF)
  • Một lệnh echo hoặc print được gọi trước header()
  • HTML ngoài thẻ PHP được in ra trước khi redirect
  • Gọi session_start() sau khi output đã bắt đầu
  • Một lệnh include hoặc require âm thầm xuất ra nội dung nào đó

Quy trình debug

Bước 1: Đọc kỹ thông báo lỗi

PHP cho bạn biết chính xác nơi output bắt đầu. Tập trung vào phần này:

(output started at /var/www/html/includes/config.php:1)

Mở file đó, đến dòng đó. Đó là hiện trường vụ án.

Bước 2: Kiểm tra khoảng trắng trước <?php

Mở file bằng hex editor, hoặc chạy lệnh:

cat -A /var/www/html/includes/config.php | head -5

Thấy ^M hoặc dấu $ trên một dòng trước <?php? Bất kỳ ký tự nào trước thẻ? Đó là vấn đề của bạn. Chỉ một dấu cách lạc lõng là đủ để phá hỏng mọi thứ.

Bước 3: Kiểm tra UTF-8 BOM

Chạy lệnh:

file /var/www/html/includes/config.php

Nếu kết quả hiển thị UTF-8 Unicode (with BOM), đó chính là thủ phạm. Ba byte vô hình đó được gửi như output trước khi PHP thậm chí bắt đầu chạy.

Xóa chúng bằng lệnh:

sed -i '1s/^\xEF\xBB\xBF//' /var/www/html/includes/config.php

Hoặc mở trong VS Code — góc dưới bên phải — và chuyển encoding từ UTF-8 with BOM sang UTF-8 thông thường.

Bước 4: Tìm output xuất hiện sớm trong code

Tìm kiếm bất kỳ lệnh echo, print, hoặc HTML thuần xuất hiện trước các lệnh gọi header:

grep -n 'echo\|print\|?>' /var/www/html/login.php

Đây là mẫu code kinh điển phá hỏng mọi thứ:

<!-- sai: HTML trước redirect -->
<html>
<head><title>Login</title></head>
<?php
session_start(); // QUÁ MUỘN — HTML đã được gửi rồi
header('Location: /dashboard.php'); // Lệnh này sẽ thất bại
?>

Giải pháp

Cách 1: Chuyển tất cả header lên trước mọi output

Quy tắc đơn giản: header(), setcookie(), và session_start() phải đến trước bất cứ thứ gì tạo ra output — kể cả một dòng trống.

<?php
// ĐÚNG: session và header đặt đầu tiên, không có gì trước dòng này
session_start();

if (!isset($_SESSION['user'])) {
    header('Location: /login.php');
    exit(); // Luôn exit sau khi redirect
}
?>
<!DOCTYPE html>
<html>
...

Cách 2: Dùng output buffering

Tái cấu trúc file lúc 2 giờ sáng cảm giác khá rủi ro. Output buffering giúp bạn có thêm thời gian:

<?php
ob_start(); // Giữ toàn bộ output trong bộ nhớ

// echo/print không còn gửi header ngay lập tức nữa
echo 'something';

// Lệnh này hoạt động được rồi
header('Location: /dashboard.php');
ob_end_flush();
exit();
?>

ob_start() giữ output trong bộ nhớ thay vì đẩy ngay ra trình duyệt. Header vẫn có thể chỉnh sửa cho đến khi buffer được xả ra. Đây không phải giải pháp lâu dài — nhưng là biện pháp tình thế hiệu quả.

Bạn cũng có thể bật output buffering toàn cục trong php.ini:

output_buffering = 4096

Sau đó khởi động lại tiến trình PHP:

sudo systemctl restart php8.2-fpm
# hoặc
sudo systemctl restart apache2

Cách 3: Bỏ thẻ đóng PHP

Bất kỳ ký tự xuống dòng nào sau thẻ đóng ?> đều được tính là output. Cách khắc phục: xóa nó đi. Thẻ đóng là tùy chọn trong PHP — và với các file PHP thuần, bỏ thẻ đóng là thực hành được khuyến nghị:

<?php
// File PHP thuần — không cần thẻ đóng ?>
define('DB_HOST', 'localhost');
define('DB_NAME', 'myapp');
// Kết thúc file — không có ?>

Cách 4: Tìm BOM ẩn trong các file được include

BOM có thể không nằm trong file chính. Hãy kiểm tra tất cả các file được include:

find /var/www/html -name '*.php' | xargs grep -l $'\xef\xbb\xbf'

Xóa BOM khỏi tất cả file PHP trong một lần:

find /var/www/html -name '*.php' -exec sed -i '1s/^\xEF\xBB\xBF//' {} \;

Xác nhận đã sửa xong

Kiểm tra trực tiếp luồng bị lỗi. Với redirect, kiểm tra response header bằng curl:

curl -I http://yoursite.com/login.php

Một redirect hoạt động đúng trông như thế này:

HTTP/1.1 302 Found
Location: /dashboard.php

Vẫn thấy 200 OK mà không có header Location? Output vẫn đang rò rỉ ở đâu đó.

Tạm thời bật báo cáo lỗi đầy đủ để phát hiện mọi cảnh báo:

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

PHP sẽ cho bạn biết chính xác file và dòng nào là nguồn gốc của output ngoài ý muốn. Tìm ra nó, loại bỏ nó, xong.

Bài học rút ra

  • Đặt session_start()header() ở vị trí tuyệt đối đầu tiên — trước bất kỳ HTML, echo, hay khoảng trắng nào.
  • Bỏ thẻ đóng ?> trong các file PHP thuần. Đó là cái bẫy khoảng trắng chực chờ kích hoạt.
  • Cấu hình editor lưu file dưới dạng UTF-8 không có BOM. UTF-8 BOM vô hình, âm thầm, và có thể ngốn của bạn cả tiếng đồng hồ debug.
  • Dùng ob_start() ở điểm vào của ứng dụng như một mạng lưới an toàn trong quá trình phát triển — không phải quyết định kiến trúc lâu dài.
  • Khi một file include bị chỉ ra trong lỗi, hãy nhớ: ngay cả một dòng trống bên ngoài thẻ PHP trong file đó là đủ để kích hoạt lỗi này.

Related Error Notes