در یکی از اولین پروژه‌های جدی که با PHP انجام دادم، یک روز صبح تمام صفحات سایت خطای زردرنگ دادند: Cannot modify header information - headers already sent. کارفرما فوری زنگ زد و من در همان لحظه یاد گرفتم که این خطا، هرچند در ظاهر بی‌خطر است، اما می‌تواند کل معماری یک اپلیکیشن را افشا کند. خطای Cannot modify header information در PHP از آن دسته خطاهایی است که در نگاه اول یک باگ کوچک به‌نظر می‌رسد، ولی وقتی ریشه‌اش را بفهمید، متوجه می‌شوید که علامت یک تصمیم معماری اشتباه یا نبود نظم در ساختار کد است.

خطای Cannot modify header information چیست؟

در PHP، زمانی که اسکریپت شما با استفاده از توابعی مثل header()، setcookie()، یا session_start() قصد تغییر هدرهای HTTP را دارد، این تغییرات باید قبل از ارسال هر نوع خروجی به مرورگر ارسال شود. اگر هر خروجی - حتی یک کاراکتر فاصله، یک خط جدید، یا یک کاراکتر BOM - قبل از فراخوانی این توابع به مرورگر ارسال شود، PHP خطای زیر را ثبت می‌کند:

Warning: Cannot modify header information - headers already sent by (output started at /path/to/file.php:N) in /path/to/other.php on line M

این خطا از نوع Warning است، نه Fatal. یعنی اسکریپت متوقف نمی‌شود ولی هدرهای HTTP شما ارسال نمی‌شوند. نتیجه‌ی نهایی: کاربر ممکن است صفحه را ببیند ولی بدون ریدایرکت، بدون کوکی جدید، و بدون session. این وضعیت، در ظاهر بی‌خطر است، ولی می‌تواند منجر به مشکلات جدی مثل عدم احراز هویت، از دست رفتن session، و تجربه‌ی کاربری شکسته شود.

نکته‌ی کلیدی که در پیام خطا پنهان است: عبارت output started at. PHP می‌گوید کدام فایل و کدام خط، اولین خروجی را فرستاده است. این دقیقاً همان جایی است که باید به‌عنوان مبدأ خطا در نظر بگیرید، نه فایلی که در آن فراخوانی header() انجام شده. توجه به این تفکیک، نیمی از راه تشخیص است.

اگر با مبانی PHP آشنایی ندارید، ابتدا آموزش PHP از صفر برای مبتدیان را بخوانید تا مدل ذهنی درستی از ساختار اسکریپت‌های PHP شکل بگیرد.

هدرهای HTTP شبیه به پاکت نامه هستند: باید قبل از باز کردن نامه نوشته شوند. اگر نامه را باز کردید، دیگر نمی‌توانید روی پاکت چیزی بنویسید.

هدرهای HTTP و مفهوم خروجی در PHP

برای درک عمیق این خطا، باید نحوه‌ی ارتباط PHP با مرورگر را بشناسیم. وقتی مرورگر درخواستی به سرور می‌فرستد، PHP پردازش را شروع می‌کند. اما فرآیند پاسخ‌دهی، دو بخش دارد: هدرها و بدنه.

هدرها، اطلاعاتی متا درباره‌ی پاسخ هستند: نوع محتوا (Content-Type)، کد وضعیت (Status Code)، کوکی‌ها (Set-Cookie)، محل ریدایرکت (Location)، و اطلاعات مربوط به caching. این هدرها به‌طور خودکار یا صریح، توسط کد شما تعیین می‌شوند.

بدنه، محتوای اصلی پاسخ است: HTML، JSON، یا هر نوع داده‌ای که به کاربر نمایش داده می‌شود.

پروتکل HTTP یک قانون بنیادین دارد: هدرها باید همیشه قبل از بدنه ارسال شوند. PHP نمی‌تواند هدرها را بعد از شروع بدنه تغییر دهد چون مرورگر از قبل هدرها را دریافت کرده و منتظر بدنه است. این محدودیت، از پروتکل HTTP می‌آید، نه از PHP. هرچه این محدودیت را در ذهن داشته باشید، درک خطا آسان‌تر می‌شود.

نکته‌ی ظریف: هر نوع خروجی از PHP، حتی یک خط خالی، حتی یک BOM در ابتدای فایل، عملاً به‌عنوان ارسال بدنه تلقی می‌شود. از دید PHP، این خروجی واقعی است و مرز بین هدر و بدنه را رد کرده. بنابراین هر خروجی قبل از توابع header، باعث این خطا می‌شود. این مفهوم در مباحث عمومی خطای Warning در PHP هم به‌عنوان یک الگوی تکراری دیده می‌شود.

چرا این خطا رخ می‌دهد؟

پاسخ ساده این است: چون خروجی قبل از هدر ارسال شده. ولی این پاسخ، عمق مسئله را نشان نمی‌دهد. در تجربه‌ی من، ریشه‌ی این خطا در سه لایه‌ی متفاوت است:

لایه‌ی اول: کد نوشته‌شده‌ی خود شما. مثلاً echo قبل از header(). این لایه، واضح‌ترین و قابل‌رفع‌ترین است.

لایه‌ی دوم: فایل‌های جانبی که require می‌شوند. یک فایل کوچک که کنار اسکریپت اصلی require شده و یک خط خالی در ابتدا یا انتها دارد، می‌تواند این خطا را ایجاد کند، حتی اگر خود اسکریپت اصلی تمیز باشد.

لایه‌ی سوم: تنظیمات محیط اجرا. در بعضی از سرورها، PHP خودش یک خروجی مقدماتی ارسال می‌کند (مثل BOM یا گواهی SSL). این خروجی‌ها از دید کد شما پنهان هستند.

پس در تشخیص این خطا، باید هر سه لایه را بررسی کنید. خیلی وقت‌ها توسعه‌دهنده به لایه‌ی اول نگاه می‌کند، آن را تمیز می‌بیند و به سمت گوگل می‌رود. در حالی که ریشه در لایه‌ی دوم یا سوم بوده.

شش علت اصلی این خطا

در تجربه‌ی من روی صدها پروژه، خطای Cannot modify header information از چند علت مشخص می‌آید. شناخت این علت‌ها، تشخیص را در چند ثانیه ممکن می‌کند.

علت اول: خروجی قبل از هدر

شایع‌ترین علت. کدی مثل این:

echo "Starting...";
header("Location: /home");

این الگو در کدهای مبتدی شایع است، ولی در کدهای حرفه‌ای هم گاهی به‌اشتباه رخ می‌دهد، مخصوصاً وقتی که یک شرط منطقی باعث می‌شود مسیر اجرا از خروجی رد شود و بعد به header برسد.

علت دوم: فاصله یا خط خالی در ابتدای فایل include

یک فایل که با یک خط خالی شروع می‌شود، باعث می‌شود آن خط به‌عنوان خروجی به مرورگر برود. این فاصله‌ها اغلب نادیده گرفته می‌شوند چون در ویرایشگر کد، visible نیستند.

علت سوم: BOM در ابتدای فایل

فایل‌هایی که با ویرایشگرهای ویندوزی مثل Notepad ذخیره می‌شوند، ممکن است یک BOM در ابتدا داشته باشند. این سه بایت مخفی، قبل از هر کد PHP، به مرورگر ارسال می‌شوند. تشخیص این مشکل در محیط‌های Unix سخت‌تر است، چون ویرایشگرهای استاندارد BOM را نمایش نمی‌دهند.

علت چهارم: session_start بعد از خروجی

session_start() یکی از توابعی است که هدر ارسال می‌کند - چون کوکی session_id را تنظیم می‌کند. اگر قبل از آن خروجی ارسال شده باشد، همان خطا رخ می‌دهد. جزئیات کامل در خطای session_start در PHP آمده است.

علت پنجم: redirect بدون exit

اگر در کد شما بعد از header("Location: ...") دستور exit یا die نباشد، ادامه‌ی کد اجرا می‌شود و ممکن است دوباره header() فراخوانی شود یا خروجی تولید شود. این الگو در کدهای اولیه، معمولاً باعث رفتار غلط می‌شود که ظاهر آن، همین خطای Cannot modify header information است.

علت ششم: output buffering غیرفعال

Output Buffering که در ادامه به‌تفصیل بررسی می‌شود، یک مکانیزم است که خروجی را در حافظه نگه می‌دارد تا بعداً ارسال شود. اگر این مکانیزم غیرفعال باشد، هر خروجی مستقیم به مرورگر می‌رود. بعضی از هاست‌ها به‌طور پیش‌فرض Output Buffering را غیرفعال می‌کنند، و این باعث می‌شود خطاهایی که در محیط توسعه پنهان بودند، در production ظاهر شوند.

فاصله سفید و BOM: دشمنان پنهان

در تجربه‌ی من، پیچیده‌ترین موارد این خطا از فاصله یا BOM می‌آیند، چون به‌سختی قابل مشاهده هستند و اغلب نادیده گرفته می‌شوند.

فاصله سفید بعد از ?> در انتهای فایل

در PHP، خطای مرسوم این است که فایل‌ها با ?> بسته شوند و بعد یک خط خالی باقی بماند. این خط خالی، به‌عنوان خروجی به مرورگر ارسال می‌شود و در includeهای بعدی باعث خطا می‌شود.

راه‌حل حرفه‌ای: در فایل‌های PHP که خروجی HTML ندارند (یعنی فایل‌هایی که فقط logic هستند)، هرگز ?> را نگذارید. این رویه در استانداردهای PSR-2 و PSR-12 تأکید شده است. فایل PHP خالی از ?>، نمی‌تواند خطای فاصله سفید تولید کند.

BOM در ابتدای فایل

BOM یا Byte Order Mark، سه بایت مخفی در ابتدای فایل است که بعضی ویرایشگرها برای تشخیص encoding اضافه می‌کنند. این سه بایت، در ظاهر نامرئی هستند ولی از دید PHP، یک خروجی محسوب می‌شوند.

تشخیص BOM: با دستور خط فرمان زیر می‌توانید فایل‌هایی که BOM دارند را پیدا کنید:

grep -rl $'\xef\xbb\xbf' .

این دستور، فایل‌هایی که با BOM شروع می‌شوند را لیست می‌کند. برای حذف BOM، می‌توانید از sed یا ویرایشگرهای مخصوص استفاده کنید.

پیشگیری: تنظیم ویرایشگر به UTF-8 without BOM. این تنظیم یک‌بار انجام می‌شود و از بروز مکرر مشکل جلوگیری می‌کند. اگر با تیم کار می‌کنید، این تنظیم را به‌عنوان بخشی از استاندارد پروژه تعریف کنید.

Session و Cookie دو مفهوم مرتبط با هدر هستند که در بسیاری از پروژه‌ها منبع این خطا می‌شوند.

ترتیب درست در کد

الگوی درست:



...

در این الگو، session_start و header قبل از هر خروجی HTML اجرا می‌شوند. exit بعد از header، از ادامه‌ی اجرا جلوگیری می‌کند.

تنظیمات session در php.ini

در بعضی موارد، session.auto_start در php.ini فعال است. این گزینه باعث می‌شود PHP خودش جلسه را در ابتدای هر اسکریپت شروع کند. اگر به‌درستی مدیریت نشود، می‌تواند با کد شما تضاد داشته باشد. توصیه: session.auto_start = 0 و مدیریت جلسه در کد.

کوکی‌ها و Same-Site Policy

در PHP 7.3 به بعد، می‌توانید پارامتر SameSite را در کوکی‌ها تنظیم کنید. این پارامتر از نظر امنیتی مفید است ولی اگر اشتباه تنظیم شود، می‌تواند باعث رفتار نامنظم در session شود. مبانی امنیتی در امنیت در PHP به‌تفصیل آمده است.

Redirect و Location Header

ریدایرکت یکی از پرکاربردترین کاربردهای header() است، ولی در عین حال یکی از پرخطرترین نیز. الگوی درست ریدایرکت:

نکته‌ی حیاتی: exit بعد از header. بدون exit، ادامه‌ی کد اجرا می‌شود و ممکن است باعث خروجی یا خطا شود. این اشتباه در کدهای آموزشی رایج است، چون در مثال‌های ساده، ادامه‌ی کد خروجی مهمی تولید نمی‌کند؛ ولی در پروژه‌های واقعی، ادامه‌ی کد می‌تواند باعث نشت داده یا خطا شود.

نکته‌ی دیگر: در ریدایرکت، کد وضعیت را با پارامتر سوم تعیین کنید. 301 برای ریدایرکت دائمی، 302 یا 307 برای موقت. این تفکیک برای سئو و کش مرورگر مهم است. اگر سایت شما ریدایرکت‌های زیادی دارد، مباحث مربوط به هدرهای ارسال‌شده می‌تواند مفید باشد.

روش تشخیص سریع و اصولی

تشخیص این خطا، در بیشتر موارد ساده است، به شرطی که پیام را با دقت بخوانید. سه گام تشخیص من:

گام اول: خواندن پیام خطا

پیام خطا دو مسیر می‌دهد: مسیری که در آن خروجی شروع شده، و مسیری که در آن header فراخوانی شده. مسیر اول مهم‌تر است. آنجا نقطه‌ی شروع مشکل است.

گام دوم: بررسی فایل مورد اشاره

فایل اشاره‌شده در output started at را باز کنید. اگر خط اول فایل خالی است، همان مشکل است. اگر خط اول یک echo یا HTML دارد، آن خط را حذف یا جابه‌جا کنید. اگر فایل با ?> بسته شده و بعد فاصله دارد، ?> را حذف کنید.

گام سوم: جستجوی BOM

اگر فایل تمیز به‌نظر می‌رسد، با ابزارهای search، BOM را در تمام پروژه جستجو کنید. دستور Linux در بخش قبل آمده است. برای پروژه‌های بزرگ، این جستجو چند ثانیه طول می‌کشد ولی می‌تواند چند ساعت زمان دیباگ را صرفه‌جویی کند.

ابزار تشخیص پیشرفته: headers_sent

تابع headers_sent() به شما می‌گوید آیا هدرها ارسال شده‌اند یا نه:

if (headers_sent($file, $line)) {
    error_log("Headers sent at $file:$line");
}

این تابع، یک ردیاب دقیق در زمان اجرا فراهم می‌کند. در پروژه‌های پیچیده که پیدا کردن منبع خروجی سخت است، این ابزار تفاوت جدی ایجاد می‌کند. مبانی کامل مدیریت خطا در PHP در مدیریت خطا در PHP آمده است.

Output Buffering: راه‌حل کلاسیک و محدودیت‌ها

Output Buffering یا بافر خروجی، یک مکانیزم است که PHP خروجی را در حافظه نگه می‌دارد و تا زمانی که تصمیم نگیرید، به مرورگر نمی‌فرستد. این مکانیزم، راه‌حل کلاسیک این خطاست.

چگونه فعال می‌شود؟

با ob_start() در ابتدای اسکریپت، خروجی‌ها در بافر ذخیره می‌شوند. با ob_end_flush() در انتها، بافر به مرورگر ارسال می‌شود. در بین این دو، می‌توانید هدرها را آزادانه تغییر دهید.

تنظیم سراسری در php.ini

output_buffering = 4096

با تنظیم این مقدار، PHP به‌طور خودکار یک بافر خروجی با اندازه‌ی مشخص فعال می‌کند. این رویکرد، رایج‌ترین راه‌حل در سرورهای production است. مقدار 4096 یعنی بافر 4 کیلوبایتی. تا زمانی که خروجی از این مقدار کمتر باشد، ارسال نمی‌شود.

محدودیت‌های Output Buffering

Output Buffering یک راه‌حل مفید است، ولی معایبی دارد:

  1. حافظه‌ی اضافی: بافر، داده را در حافظه نگه می‌دارد. برای خروجی‌های بزرگ، این می‌تواند فشار حافظه ایجاد کند.
  2. تجربه‌ی کاربری: با بافر، کاربر همه‌ی صفحه را یک‌جا دریافت می‌کند، نه تکه‌تکه. برای صفحات بزرگ، این می‌تواند باعث تأخیر در نمایش اولیه شود.
  3. پوشاندن مشکل: Output Buffering، خطا را پنهان می‌کند بدون اینکه ریشه را حل کند. اگر کد شما خروجی قبل از header تولید می‌کند، Output Buffering فقط آن را در حافظه نگه می‌دارد، ولی ریشه‌ی مشکل باقی می‌ماند.
  4. ناسازگاری با streaming: در بعضی از سناریوها مثل streaming فایل‌های بزرگ، Output Buffering اختلال ایجاد می‌کند.

به‌همین دلیل، Output Buffering را به‌عنوان راه‌حل موقت در نظر بگیرید، نه راه‌حل دائمی. راه‌حل پایدار، معماری درست است که در بخش بعدی باز می‌شود. الگوهای مرتبط در مباحث خطای Maximum execution time هم دیده می‌شود - چون هر دو مسئله، وقتی بهینه‌سازی نمی‌شوند، اثر هم‌افزوده دارند.

Output Buffering، شبیه به یک پوشک برای کد شماست. کار را راه می‌اندازد، ولی به‌جای یادگیری استفاده از دستشویی، فقط نگه می‌دارد.

راه‌حل معماری: جداسازی منطق از خروجی

راه‌حل پایدار این خطا، در معماری کد است. الگوی حرفه‌ای، جداسازی منطق برنامه از خروجی HTML است.

الگوی Controller-Before-View

در این الگو، تمام منطق برنامه (شامل تمام headerها و redirectها) قبل از شروع خروجی HTML اجرا می‌شود:




Dashboard

    

Welcome,

در این الگو، هر چیزی که به هدر مربوط می‌شود، قبل از هر خروجی HTML اجرا می‌شود. این رویکرد، قلب معماری MVC است که در فریم‌ورک‌هایی مثل Laravel و Symfony استاندارد شده.

الگوی Front Controller

در پروژه‌های بزرگ، استفاده از یک front controller که تمام درخواست‌ها از آن عبور می‌کنند، تفاوت جدی ایجاد می‌کند:

handle();

// در این نقطه، همه‌ی headerها تنظیم شده‌اند
if ($response->shouldRedirect()) {
    header("Location: " . $response->getRedirectUrl());
    exit;
}

// خروجی نهایی
ob_end_clean();
echo $response->getContent();

این الگو، تمام منطق را قبل از خروجی اجرا می‌کند و در نهایت یک خروجی واحد تولید می‌کند. این ساختار، فریم‌ورک‌های مدرن را از اسکریپت‌های پراکنده جدا می‌کند.

جداسازی فایل‌ها

در پروژه‌های مبتنی بر include، این رویکرد را دارم: فایل‌هایی که فقط logic دارند (مثل config، functions، classes) هیچ HTML یا فاصله اضافی ندارند و با ?> بسته نمی‌شوند. فایل‌هایی که خروجی HTML دارند، از ?> در انتها استفاده می‌کنند ولی بعد از آن هیچ فاصله‌ای ندارند.

فایل‌های view جداگانه

در الگوی MVC، فایل‌های view در یک پوشه‌ی جداگانه هستند و از طریق کنترلر load می‌شوند. این جدا‌سازی، از بروز خطاهایی مثل خروجی قبل از header جلوگیری می‌کند چون تمام headerها در کنترلر تنظیم می‌شوند، قبل از اینکه view شروع به تولید خروجی کند.

استفاده از headers_sent و headers_list

PHP دو تابع مفید برای کار با هدرها دارد که در تشخیص و پیشگیری موثرند:

headers_sent

این تابع، بررسی می‌کند که هدرها ارسال شده‌اند یا نه:

if (!headers_sent($file, $line)) {
    header("X-Custom: value");
} else {
    error_log("Cannot send header, output started at $file:$line");
}

در پروژه‌های حساس، این چک قبل از هر header() می‌تواند جلوی خطا را بگیرد. این رویکرد، یک لایه‌ی دفاعی است که در پروژه‌های سازمانی توصیه می‌شود.

headers_list

این تابع، لیست هدرهایی که تا الان تنظیم شده‌اند را برمی‌گرداند:

$headers = headers_list();
foreach ($headers as $header) {
    error_log($header);
}

این تابع، در دیباگ مفید است. با مشاهده‌ی لیست هدرها، می‌فهمید که کدام هدرها به‌طور خودکار توسط PHP یا سرور تنظیم شده‌اند.

الگوی پیشگیرانه

الگویی که در پروژه‌های انتقادی استفاده می‌کنم:

function safe_header($header, $replace = true, $code = null) {
    if (headers_sent($file, $line)) {
        error_log("Header failed: $header. Output started at $file:$line");
        return false;
    }
    header($header, $replace, $code);
    return true;
}

این الگو، هدر را فقط در صورت امکان ارسال می‌کند و در صورت شکست، در لاگ ثبت می‌کند. این رویکرد، در پروژه‌هایی که خطای هدر می‌تواند به تجربه‌ی کاربر آسیب بزند، مفید است.

رفتار این خطا در فریم‌ورک‌ها و وردپرس

فریم‌ورک‌های PHP و سیستم‌هایی مثل وردپرس، راه‌حل‌های خاصی برای این خطا دارند که شناخت آن‌ها مفید است.

Laravel و Symfony

در این فریم‌ورک‌ها، معماری MVC به‌طور پیش‌فرض خطا را به‌حداقل می‌رساند. تمام پاسخ‌ها از طریق Response object ساخته می‌شوند و هدرها قبل از ارسال body تنظیم می‌شوند. اگر با این فریم‌ورک‌ها کار می‌کنید و این خطا را می‌بینید، احتمالاً یک فایل خارج از الگوی فریم‌ورک این خطا را تولید کرده - مثلاً یک helper function که به‌اشتباه echo دارد.

وردپرس

وردپرس معماری متفاوتی دارد. با وجود اینکه ساختار مشابهی دارد، بعضی از افزونه‌ها و قالب‌ها به‌دلیل کد قدیمی، این خطا را ایجاد می‌کنند. اگر با وردپرس کار می‌کنید:

  1. در فایل wp-config.php، مطمئن شوید که قبل از تگ php فاصله‌ای نیست.
  2. در فایل‌های افزونه، بعد از ?> فاصله نباشد.
  3. برای ریدایرکت‌ها، از wp_redirect() استفاده کنید، نه header() مستقیم. تابع wp_redirect() مدیریت بهتری از هدرها دارد.

اگر با ساختار وردپرس آشنایی ندارید، وردپرس چیست نقطه‌ی شروع خوبی است. همچنین مبانی دانلود افزونه مطمئن به شما کمک می‌کند افزونه‌هایی که کد باکیفیت دارند انتخاب کنید.

CodeIgniter و سایر فریم‌ورک‌های سبک

در فریم‌ورک‌های سبک‌تر مثل CodeIgniter، کنترل کمتری روی ترتیب خروجی وجود دارد. در این حالت، Output Buffering در سطح bootstrap ضروری است. اگر پروژه‌ای با CodeIgniter دارید که این خطا را می‌بیند، تنظیمات $config["output_buffering"] را بررسی کنید.

مواجهه با این خطا در production

در محیط production، این خطا ابعاد متفاوتی دارد. در development، پیام Warning نمایش داده می‌شود و می‌توانید سریع تشخیص دهید. در production، ممکن است خطا فقط در لاگ ثبت شود ولی اثرات آن در رفتار کاربر ظاهر شود.

اثرات پنهان در production

  1. عدم ریدایرکت: کاربر به‌جای صفحه‌ی login، صفحه‌ی خالی یا صفحه‌ی اشتباه می‌بیند.
  2. عدم تنظیم کوکی: کاربر نمی‌تواند وارد شود یا session او حفظ نمی‌شود.
  3. مسائل امنیتی: اگر هدرهای امنیتی (مثل CSP یا X-Frame-Options) ارسال نشوند، سایت در معرض حمله قرار می‌گیرد.
  4. مسائل SEO: اگر ریدایرکت‌های 301 به‌درستی کار نکنند، سئو سایت آسیب می‌بیند.

روش مدیریت در production

در production، سه کار انجام می‌دهم:

یک: لاگ‌گیری ساختارمند از هر خطای هدر، با context کامل. file و line که خروجی از آن شروع شده، URL، و user id.

دو: Alerting روی نرخ این خطا. اگر نرخ در بازه‌ی کوتاه بالا رفت، بررسی فوری.

سه: بازبینی ماهانه‌ی لاگ‌ها برای پیدا کردن الگوهای تدریجی. اگر خطا به‌ندرت رخ می‌دهد، ممکن است یک مسیر خاص از کد باعث آن باشد.

تست در staging

در staging، با همان تنظیمات production (مخصوصاً تنظیمات output_buffering)، تست کنید. اگر در staging خطا رخ ندهد ولی در production رخ دهد، احتمالاً تفاوت تنظیمات PHP است.

اشتباهات رایج در برخورد با این خطا

در طول سال‌ها، اشتباهات تکراری دیده‌ام که هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: استفاده‌ی بی‌جا از @

اضافه کردن @ قبل از header() خطا را پنهان می‌کند ولی مشکل باقی می‌ماند. این رویکرد، دیباگ آینده را سخت‌تر می‌کند. راه‌حل: ریشه را پیدا کنید، نه صرفاً خطا را پنهان کنید.

اشتباه دوم: فعال‌کردن Output Buffering به‌عنوان راه‌حل دائمی

Output Buffering مفید است، ولی راه‌حل دائمی نیست. اگر کد شما خروجی قبل از header تولید می‌کند، Output Buffering فقط پنهان می‌کند. راه‌حل واقعی، جداسازی منطق از خروجی است.

اشتباه سوم: حذف exit بعد از header

بعضی توسعه‌دهنده‌ها فکر می‌کنند exit بی‌فایده است چون هدر فرستاده شده. در واقع، بدون exit، ادامه‌ی کد اجرا می‌شود و می‌تواند باعث رفتار نامنظم یا خطاهای امنیتی شود. راه‌حل: همیشه بعد از header("Location: ...")، exit بگذارید.

اشتباه چهارم: نادیده گرفتن BOM

بعضی توسعه‌دهنده‌ها BOM را جدی نمی‌گیرند، چون نامرئی است. ولی این سه بایت، دقیقاً همان چیزی است که باعث خطا می‌شود. راه‌حل: تنظیم ویرایشگر روی UTF-8 without BOM و بررسی دوره‌ای فایل‌ها.

اشتباه پنجم: بدون exit در ادمین پنل‌ها

در پنل‌های ادمین، ریدایرکت‌ها بسیار شایع هستند. اگر بعد از header("Location: ...") در پنل، exit نگذارید، ممکن است ادامه‌ی کد باعث خطای امنیتی شود - چون صفحه‌ی محافظت‌شده ممکن است بخشی از محتوا را نمایش دهد.

اشتباه ششم: نادیده گرفتن این خطا در محیط development

اگر در development این خطا را نادیده بگیرید، در production به‌شکل جدی‌تری ظاهر می‌شود. راه‌حل: در development، error_reporting = E_ALL فعال و پیام‌ها نمایش داده شود.

اشتباه هفتم: عدم پیگیری خطاهای موازی

اگر سایت شما هم این خطا را می‌دهد و هم خطاهای مرتبط مثل session_start یا Cannot modify header information در فایل‌های دیگر، احتمالاً یک مشکل سیستمی وجود دارد. راه‌حل: تمام لاگ‌ها را با هم ببینید، نه یکی‌یکی.

هر خطای Cannot modify header information، یک پیام مخفی از معماری کد شماست. اگر این پیام را با دقت بشنوید، صدای مشکلات معماری بزرگ‌تری را خواهید شنید.

پرسش‌های پرتکرار درباره خطای Cannot modify header information

این پرسش‌ها از دل تجربه‌ی عملی و جلسات مشاوره جمع‌آوری شده‌اند. پاسخ هر کدام بر اساس سناریوهای واقعی است.

چرا این خطا بعد از آپدیت PHP ظاهر می‌شود؟

چون PHP نسخه‌های جدید سختگیرانه‌تر شده‌اند. در بعضی موارد، خروجی‌هایی که در PHP 7 فقط Notice بودند، در PHP 8 به Warning تبدیل شده‌اند. تفاوت‌های نسخه‌ای در تفاوت PHP 7 و PHP 8 به‌تفصیل آمده است.

تفاوت این خطا با خطای Deprecated چیست؟

این خطا از نوع Warning است - یک عملیات فعلی اشتباه است. Deprecated یک هشدار زمان‌دار است - یک API در آینده حذف می‌شود. اگر هر دو خطا در پروژه‌ی شما ظاهر می‌شوند، مقاله‌ی خطای Deprecated در PHP را ببینید.

آیا می‌توانم این خطا را در production پنهان کنم؟

می‌توانید نمایش آن را در مرورگر خاموش کنید، ولی خطا همچنان در لاگ ثبت می‌شود. پنهان کردن نمایش، راه‌حل نیست. راه‌حل واقعی، یافتن و رفع ریشه است.

چرا این خطا در بعضی صفحات سایت ظاهر می‌شود ولی در بعضی دیگر نه؟

چون در بعضی از مسیرهای اجرا، خروجی قبل از header رخ می‌دهد و در بعضی دیگر، نه. اگر این خطا در یک صفحه‌ی خاص ظاهر می‌شود، احتمالاً آن صفحه از یک فایل مشترک استفاده می‌کند که در ابتدایش فاصله یا BOM دارد.

چگونه می‌توانم تمام فایل‌های PHP که BOM دارند را پیدا کنم؟

در Linux/Mac:

find . -name "*.php" -exec sh -c "head -c 3 \"$1\" | xxd | grep -q \"efbb bf\" && echo $1" _ {} \;

این دستور، فایل‌هایی که با BOM شروع می‌شوند را لیست می‌کند. روی Windows، از ابزارهایی مثل Notepad++ استفاده کنید که BOM را با BOM نمایش می‌دهند.

آیا این خطا در JavaScript یا CSS هم رخ می‌دهد؟

نه، این خطا مخصوص PHP و هدرهای HTTP است. ولی BOM در فایل‌های JavaScript می‌تواند باعث مشکلات در تفسیر توسط مرورگر شود که به‌شکل‌های دیگری بروز می‌کند.

چگونه در وردپرس این خطا را تشخیص دهم؟

اول، در wp-config.php مطمئن شوید که قبل از تگ php فاصله‌ای نیست. دوم، در پوشه‌ی wp-content/plugins/ و wp-content/themes/، فایل‌هایی که با BOM شروع می‌شوند را پیدا کنید. سوم، از ابزارهایی مثل Query Monitor برای دیدن خطاهای PHP در وردپرس استفاده کنید.

تفاوت Output Buffering و ob_start چیه؟

output_buffering در php.ini یک تنظیم سراسری است که به‌طور خودکار فعال می‌شود. ob_start() یک فراخوانی صریح در کد است. تفاوت اصلی: تنظیم سراسری معمولاً بعد از رسیدن به بافر flush می‌شود، ولی ob_start() تا فراخوانی ob_end_flush() ادامه پیدا می‌کند.

آیا Output Buffering روی performance اثر دارد؟

بله، مقدار کمی. Output Buffering خروجی را در حافظه نگه می‌دارد و در نهایت یک‌جا ارسال می‌کند. برای صفحات معمولی، اثر ناچیز است. برای صفحات با محتوای بسیار زیاد، می‌تواند مصرف حافظه را افزایش دهد. اگر مشکل حافظه دارید، خطای Memory limit در PHP را ببینید.

چگونه BOM را از یک فایل حذف کنم؟

روی Linux:

sed -i "1s/^\xEF\xBB\xBF//" file.php

روی Windows، از Notepad++ با گزینه‌ی Encode → Convert to UTF-8 without BOM. روی Mac، از ویرایشگرهای پیشرفته مثل VS Code که گزینه‌ی encoding را در پایین نمایش می‌دهد.

آیا این خطا از دید گوگل تأثیر منفی دارد؟

خود خطا در لاگ سرور ثبت می‌شود و برای گوگل به‌طور مستقیم قابل مشاهده نیست. ولی اگر باعث شکست ریدایرکت یا هدرهای امنیتی شود، خزنده‌ی گوگل رفتار اشتباه سایت شما را می‌بیند و می‌تواند روی ایندکس تأثیر بگذارد.

چرا این خطا در سایت‌های اشتراکی بیشتر دیده می‌شود؟

چون تنظیمات PHP در هاست‌های اشتراکی متنوع است. بعضی هاست‌ها output_buffering را غیرفعال می‌کنند که باعث می‌شود خطاها واضح‌تر ظاهر شوند. راه‌حل: یا تنظیمات را با پشتیبانی هاست هماهنگ کنید، یا معماری کد را طوری تنظیم کنید که خطا رخ ندهد.

آیا افزونه‌های امنیتی می‌توانند این خطا را ایجاد کنند؟

بله، بعضی از افزونه‌های امنیتی که در firewalls پیشرفته از خودشان هدر ارسال می‌کنند، می‌توانند با کد شما تضاد پیدا کنند. اگر این خطا بعد از نصب یک افزونه‌ی امنیتی ظاهر شد، ابتدا آن را غیرفعال کنید و بررسی کنید.

در فریم‌ورک‌های مدرن، چرا این خطا کم پیش می‌آید؟

چون فریم‌ورک‌های مدرن از الگوی Response object استفاده می‌کنند. تمام محتوا در حافظه ساخته می‌شود، سپس در یک‌جا ارسال می‌شود. این معماری، خطاهایی که از ترتیب خروجی ناشی می‌شوند را حذف می‌کند.

آیا می‌توانم خطا را به exception تبدیل کنم؟

بله، با set_error_handler(). ولی این کار در این مورد خاص توصیه نمی‌شود، چون خطا از نوع Warning است و معمولاً ادامه‌ی اجرا مفید است. تبدیل به exception در مواردی که خطا باعث رفتار ناسازگار شود، مفید است.

آیا این خطا با خطای سفید شدن صفحه ارتباط دارد؟

نه مستقیماً. این خطا از نوع Warning است و اسکریپت متوقف نمی‌شود. ولی اگر خطا در یک context بحرانی رخ دهد و بعداً منجر به خطای Fatal شود، می‌تواند باعث سفید شدن صفحه شود. الگوهای Fatal در رفع خطای Fatal error در PHP آمده است.

چگونه در CI جلوی این خطا را بگیرم؟

سه کار موثر: اول، یک لینتر مثل PHP_CodeSniffer با استاندارد PSR-12 در CI اجرا کنید که فایل‌های بدون BOM و بدون فاصله‌ی اضافه را تأیید می‌کند. دوم، PHPUnit را طوری تنظیم کنید که Warningهای PHP باعث fail شوند. سوم، یک تست integration که یک ریدایرکت معمولی را بررسی کند.

آیا حذف ?> در انتهای فایل PHP می‌تواند مشکل ایجاد کند؟

در فایل‌هایی که فقط logic دارند، حذف ?> توصیه می‌شود و مشکل ایجاد نمی‌کند. در فایل‌های که ترکیب PHP و HTML دارند، ?> بخشی از ساختار است و نمی‌توان حذف کرد. قاعده‌ی کلی: در فایل PHP خالص، ?> را نگذارید.

چرا خطا در بعضی از مرورگرها دیده می‌شود ولی در بقیه نه؟

چون خطا از سمت سرور است، نه مرورگر. تفاوت در این است که بعضی مرورگرها نسبت به هدرهای ناسازگار سختگیرانه‌تر عمل می‌کنند. مثلاً اگر سایت شما کوکی ناسازگار با SameSite ارسال کند، بعضی مرورگرها آن را رد می‌کنند و بعضی دیگر قبول. راه‌حل: از ابزارهایی مثل curl برای تست دقیق هدرها استفاده کنید.

آیا این خطا با HTTPS ارتباط دارد؟

غیرمستقیم. اگر سایت شما روی HTTPS است و ریدایرکت‌های HTTP به HTTPS را به‌درستی مدیریت نمی‌کنید، ممکن است این خطا رخ دهد. راه‌حل: تنظیمات سرور (Apache یا Nginx) و اطمینان از اینکه ریدایرکت‌های سرور قبل از رسیدن به PHP اتفاق می‌افتند.

چرا بعد از مهاجرت به سرور جدید این خطا ظاهر می‌شود؟

چون تنظیمات PHP در سرور جدید متفاوت است. ممکن است output_buffering غیرفعال باشد، یا نسخه‌ی PHP سختگیرانه‌تر باشد، یا BOM در فایل‌های قدیمی مشکلی که در سرور قبلی پنهان بود، در سرور جدید آشکار شده باشد. راه‌حل: مقایسه‌ی phpinfo() دو سرور و تنظیم یکسان.

آیا در CLI این خطا رخ می‌دهد؟

در CLI، هدرهای HTTP معنایی ندارند، پس این خطا معمولاً رخ نمی‌دهد. ولی اگر در CLI بخواهید از توابع header استفاده کنید، ممکن است خطای مشابه ببینید. برای اسکریپت‌های CLI، ساختار متفاوتی لازم است.

چه ابزارهایی برای دیباگ این خطا توصیه می‌کنید؟

سه ابزار اصلی: اول، headers_sent() در PHP برای پیگیری در زمان اجرا. دوم، curl با گزینه‌ی -I برای دیدن هدرها در ترمینال. سوم، ابزارهای مرورگر مثل Chrome DevTools در تب Network برای مشاهده‌ی هدرهای واقعی. ترکیب این سه، تصویر کاملی می‌دهد.

آیا این خطا در PHP-FPM رفتار متفاوتی دارد؟

در PHP-FPM، به‌دلیل تفاوت در مدیریت output buffering، این خطا می‌تواند در موقعیت‌های متفاوتی ظاهر شود. مهم‌ترین تفاوت: در PHP-FPM، هدرها معمولاً تا پایان اسکریپت نگه داشته می‌شوند و بعد ارسال می‌شوند. بنابراین، خطای هدر معمولاً در این حالت کمتر رخ می‌دهد. ولی اگر fastcgi_buffering در Nginx غیرفعال باشد، این مزیت از دست می‌رود.

آیا در PHP 8.1 به بعد این خطا سختگیرانه‌تر شده؟

خود پیام خطا تغییری نکرده، ولی PHP 8.1 به بعد در برخی شرایط سختگیرانه‌تر شده. مخصوصاً در رابطه با خواندن BOM و رفتار در سیستم‌های Unix. رفتار دقیق در changelog PHP قابل بررسی است.

چطور می‌توانم این خطا را شبیه‌سازی کنم برای تست؟

یک فایل PHP ساده بسازید:

این فایل را در staging اجرا کنید. Warning مشابهی خواهید دید. با اضافه کردن ob_start() در ابتدا و ob_end_flush() در انتها، می‌توانید تأثیر Output Buffering را هم ببینید.

آنچه از سال‌ها کار با هدرهای HTTP در PHP یاد گرفتم

اگر بخواهم چکیده‌ی این سال‌ها را در چند جمله بگویم، سه اصل عملی دارم:

یک: هدرها، پاکت نامه‌ی پروتکل HTTP هستند. یک بار بسته شدن پاکت، یعنی تمام محتوا باید در آن پاکت باشد. بعد از بسته شدن، نمی‌توانید چیز جدیدی اضافه کنید. این تصویر ذهنی، در برخورد با خطاها بسیار کمک می‌کند.

دو: معماری درست، صد برابر موثرتر از Output Buffering است. Output Buffering یک راه‌حل موقت است. راه‌حل دائمی، جداسازی منطق از خروجی است. اگر کد شما این جداسازی را رعایت کند، این خطا تقریباً از بین می‌رود.

سه: سه بایت BOM، می‌تواند ساعتی وقت شما را بگیرد. این سه بایت نامرئی، شایع‌ترین علت خطاهای گیج‌کننده است. یک بار سرمایه‌گذاری روی تنظیم ویرایشگر، و بعد از آن خیال راحت.

خطای Cannot modify header information در PHP، در نگاه اول یک باگ کوچک به‌نظر می‌رسد. ولی وقتی در چارچوب معماری HTTP دیده شود، تبدیل به یک سیگنال می‌شود. این سیگنال می‌گوید ساختار کد شما با پروتکل HTTP در تضاد است. اگر این سیگنال را جدی بگیرید و معماری را اصلاح کنید، پروژه‌ی شما به سطحی از پایداری می‌رسد که این خطا هرگز ظاهر نمی‌شود.

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، پیشگیری و رفع این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با این خطا از یک واکنش اضطراری به یک تصمیم معماری تبدیل می‌شود. و این، همان تفاوتی است که بین توسعه‌دهنده‌ی معمولی و توسعه‌دهنده‌ای که به کدش اعتماد دارد، وجود دارد.

اگر این خطا در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. 📨