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

beginner๐Ÿ˜ PHP2026-03-22| PHP 7.x / 8.x on Linux (Apache, Nginx + PHP-FPM), Windows (XAMPP, WAMP)

Error Message

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

The situation

It's 2 AM. A redirect isn't working. Users hit a blank page instead of the dashboard. The logs show this:

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

That message tells you almost everything. PHP tried to call header() โ€” or setcookie(), or session_start() โ€” but something already pushed bytes to the browser. Once output is sent, HTTP headers are locked. Done.

What triggers this

The cause is always the same: something produced output before your header call. The tricky part is that "output" can be almost invisible.

Common culprits:

  • A space or newline before the opening <?php tag
  • A UTF-8 BOM at the start of a file (invisible in most editors โ€” 3 silent bytes: EF BB BF)
  • An echo or print called before header()
  • HTML outside PHP tags printed before a redirect
  • A session_start() called after output has already started
  • An include or require that silently outputs something

Debug process

Step 1: Read the error message carefully

PHP tells you exactly where output started. Focus on this part:

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

Go to that file, that line. That's your crime scene.

Step 2: Check for whitespace before <?php

Open the file in a hex editor, or run:

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

See ^M or a $ on a line before <?php? Any character at all before the tag? That's your problem. One stray space is enough to break everything.

Step 3: Check for UTF-8 BOM

Run:

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

If the output says UTF-8 Unicode (with BOM), that's it. Those three invisible bytes get sent as output before PHP even starts.

Strip them with:

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

Or open in VS Code โ€” bottom-right corner โ€” and switch encoding from UTF-8 with BOM to plain UTF-8.

Step 4: Hunt for premature output in code

Search for any echo, print, or raw HTML appearing before your header calls:

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

This is the classic pattern that breaks everything:

<!-- bad: HTML before redirect -->
<html>
<head><title>Login</title></head>
<?php
session_start(); // TOO LATE โ€” HTML already sent
header('Location: /dashboard.php'); // This will fail
?>

Solutions

Fix 1: Move all headers before any output

Simple rule: header(), setcookie(), and session_start() must come before anything that produces output โ€” including a single blank line.

<?php
// CORRECT: session and headers first, nothing before this
session_start();

if (!isset($_SESSION['user'])) {
    header('Location: /login.php');
    exit(); // Always exit after a redirect
}
?>
<!DOCTYPE html>
<html>
...

Fix 2: Use output buffering

Restructuring the file at 2 AM feels risky. Output buffering buys you time:

<?php
ob_start(); // Hold all output in memory

// echo/print no longer immediately sends headers
echo 'something';

// This works now
header('Location: /dashboard.php');
ob_end_flush();
exit();
?>

ob_start() holds output in memory instead of flushing it to the browser. Headers stay modifiable until the buffer is flushed. It's not a permanent fix โ€” but it's a solid stopgap.

You can also enable output buffering globally in php.ini:

output_buffering = 4096

Then restart your PHP process:

sudo systemctl restart php8.2-fpm
# or
sudo systemctl restart apache2

Fix 3: Drop the closing PHP tag

Any trailing newline after a closing ?> counts as output. The fix: remove it. The closing tag is optional in PHP โ€” and for pure PHP files, omitting it is the recommended practice:

<?php
// Pure PHP file โ€” no closing ?> needed
define('DB_HOST', 'localhost');
define('DB_NAME', 'myapp');
// End of file โ€” no ?>

Fix 4: Find BOMs hiding in included files

The BOM might not be in the main file at all. Check every included file:

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

Strip the BOM from all PHP files in one shot:

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

Verify the fix

Test the broken flow directly. For a redirect, check the response headers with curl:

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

A working redirect looks like this:

HTTP/1.1 302 Found
Location: /dashboard.php

Still seeing 200 OK with no Location header? Output is still leaking somewhere.

Temporarily enable full error reporting to expose every warning:

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

PHP will tell you the exact file and line where the rogue output originates. Find it, kill it, done.

Lessons learned

  • Put session_start() and header() at the absolute top โ€” before any HTML, any echo, any whitespace.
  • Drop the closing ?> tag from PHP-only files. It's a whitespace trap waiting to fire.
  • Configure your editor to save as UTF-8 without BOM. UTF-8 BOM is invisible, silent, and will cost you an hour of debugging.
  • Use ob_start() at your app's entry point as a safety net during development โ€” not as a permanent architectural decision.
  • When an include file is blamed in the error, remember: even one blank line outside a PHP tag in that file is enough to trigger this.

Related Error Notes