Skip to content

/errors/php/cannot-modify-header-information-headers-already-sent

PHP Warning: Cannot modify header information - headers already sent by (output started at ...)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
PHP
versions
PHP 7.x, PHP 8.x
os
any

Warning: Cannot modify header information - headers already sent by (output started at /app/redirect.php:1) means PHP had already sent some output to the client when your code called header() (or setcookie(), session_start(), http_response_code()). HTTP headers must come before the body, and the first byte of output sends them. The header call is ignored, so a redirect does not happen. The part in parentheses names the file and line where output started: remove or move that output (often a blank line before <?php, a closing ?> followed by a newline, or a UTF-8 BOM), and call header() first.

Reproduce it

PHP 8.4 running in Node.js with php-wasm (the PHP interpreter compiled to WebAssembly, from the WordPress Playground project). Each script is run as a web request; the runner prints PHP's error log and the HTTP status:

bash
npm init -y >/dev/null
npm install --silent --no-audit --no-fund @php-wasm/node@3.1.56 @php-wasm/universal@3.1.56 >/dev/null 2>&1
cat > run.mjs <<'EOF'
import fs from "node:fs";
import { PHP } from "@php-wasm/universal";
import { loadNodeRuntime } from "@php-wasm/node";
const php = new PHP(await loadNodeRuntime("8.4", { emscriptenOptions: { processId: 1 } }));
php.mkdir("/app");
for (const f of process.argv.slice(2)) {
  php.writeFile(`/app/${f}`, fs.readFileSync(f));
  const r = await php.runStream({ scriptPath: `/app/${f}` });
  console.log(`--- ${f} (HTTP ${await r.httpStatusCode})`);
  process.stdout.write(await r.stderrText);
}
process.exit(0);
EOF
# an empty line before <?php
printf '\n<?php\nheader("Location: /login");\nexit;\n' > redirect.php
# a UTF-8 byte order mark before <?php
printf '\xef\xbb\xbf<?php\nsession_start();\nheader("Location: /login");\n' > bom.php
# echo before header()
printf '<?php\necho "Redirecting...";\nheader("Location: /login");\n' > echo.php
# fixed: nothing before the header call
printf '<?php\nheader("Location: /login");\nexit;\n' > fixed.php
node run.mjs redirect.php bom.php echo.php fixed.php

Output

--- redirect.php (HTTP 200)
PHP Warning:  Cannot modify header information - headers already sent by (output started at /app/redirect.php:1) in /app/redirect.php on line 3
--- bom.php (HTTP 200)
PHP Warning:  session_start(): Session cannot be started after headers have already been sent in /app/bom.php on line 2
PHP Warning:  Cannot modify header information - headers already sent by (output started at /app/bom.php:1) in /app/bom.php on line 3
--- echo.php (HTTP 200)
PHP Warning:  Cannot modify header information - headers already sent by (output started at /app/echo.php:2) in /app/echo.php on line 3
--- fixed.php (HTTP 302)

The status is 200 for the broken scripts because the Location header was never sent; only the fixed script answers 302. session_start() has its own variant of the warning, Session cannot be started after headers have already been sent, with the same cause.

Find the output

Read the part in parentheses: output started at FILE:LINE. Common culprits:

What is on that lineFix
An empty line or space before <?phpDelete it; <?php must be the first bytes of the file
Line 1 of a file that looks correctA UTF-8 BOM: save the file as UTF-8 without BOM
A closing ?> at the end of an included fileRemove the closing tag in files that contain only PHP
echo, print, var_dump, HTMLMove the header() call above it
A Warning or Notice messageFix that error first; displayed errors are output too

To check for a BOM on the command line:

bash
head -c 3 file.php | od -An -tx1    # ef bb bf means the file starts with a BOM

Output buffering

ob_start() at the very top of the script (or output_buffering = 4096 in php.ini) holds output in memory, so headers can still be changed later. It hides the symptom; fixing the stray output is better, because a buffer that fills up or is flushed brings the warning back.

Sources