خطای headers already sent یکی از آن خطاهای PHP است که بیشتر از کد، از رفتار مرورگر و ذهن توسعه‌دهنده پرده برمی‌دارد. در پروژه‌های واقعی، این خطا معمولاً در لحظه‌ای ظاهر می‌شود که مشغول کار روی یک قابلیت دیگر هستید و ناگهان با هشدارهای عجیب در بالای صفحه یا صفحه سفید مواجه می‌شوید. اولین بار که این خطا را در یک پروژه واقعی دیدم، در یک افزونه اختصاصی بود که روی سایت سازمانی با هزاران کاربر نصب شده بود و ریشه مشکل، یک بایت نامرئی در ابتدای فایل PHP بود؛ بایتی که حتی در ویرایشگر متن هم دیده نمی‌شد ولی رفتار کل افزونه را از کار انداخته بود.

اگر با مفاهیم پایه PHP در وردپرس آشنایی کمتری دارید، پیش از ادامه وردپرس چیست و چگونه شروع به کار با آن کنیم؟ را بخوانید. این نوشته، لایه عیب‌یابی همان بحث است. برای درک ارتباط این خطا با سایر خطاهای PHP، پیشنهاد می‌کنم ابتدا نوشته‌های خطای Object could not be converted to string در PHP وردپرس و خطای Cannot modify header information در PHP را مطالعه کنید چون هر سه از یک خانواده ریشه‌ای می‌آیند.

خطای headers already sent دقیقاً چه می‌گوید؟

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

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

سه چیز در این پیام مهم است. اول، فایلی که خروجی از آن شروع شده: در مثال بالا file.php در خط ۱۲. این فایل، مقصر اصلی است نه فایلی که خطا در آن گزارش شده. دوم، شماره خط دقیق در همان فایل مقصر. سوم، فایلی که در آن تلاش برای ارسال هدر انجام شده: در مثال بالا other.php در خط ۴۵.

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

پیام در نسخه‌های مختلف PHP، شکل‌های متفاوتی دارد. در PHP 8، پیام با جزئیات بیشتری همراه است و معمولاً اشاره دقیقی به فایل و خطی دارد که خروجی از آن شروع شده. در PHP 5 و 7، پیام کمی کوتاه‌تر بود ولی اطلاعات اصلی همان را داشت. جدول زیر خلاصه تفاوت این پیام‌ها در نسخه‌های مختلف را نشان می‌دهد:

نسخه PHPنوع پیامپیامد روی سایت
PHP 5.xWarningادامه اجرا با هشدار
PHP 7.0 تا 7.3Warning با جزئیات بیشترادامه اجرا با هشدار
PHP 7.4Warning با Trace کاملادامه اجرا با هشدار
PHP 8.0 به بعدWarning با اشاره دقیق به فایل مقصرادامه اجرا با هشدار واضح‌تر

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

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

چرا PHP این خطا را برمی‌گرداند؟

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

هدرها، پنجره ارتباط با مرورگر

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

در PHP، توابعی که هدر ارسال می‌کنند شامل header()، setcookie()، session_start()، wp_redirect() و wp_safe_redirect() هستند. هر یک از این توابع، اگر بعد از شروع خروجی فراخوانی شوند، این خطا را تولید می‌کنند. در وردپرس، تعداد بیشتری از توابع غیرمستقیم هم وجود دارند که در نهایت به این توابع می‌رسند، مثل wp_set_auth_cookie() در جریان ورود کاربر و wp_redirect() در هوک‌های مختلف.

خروجی، هر چیزی حتی فضای خالی

یک نکته ظریف که اکثر توسعه‌دهندگان تازه‌کار نمی‌دانند: خروجی در PHP فقط به‌معنی echo یا print نیست. هر چیزی که به خروجی فرستاده شود، خروجی محسوب می‌شود: یک کاراکتر فضای خالی، یک newline، یک tab، و حتی یک BOM (Byte Order Mark) که در ابتدای فایل قرار می‌گیرد. همین گستردگی تعریف خروجی است که این خطا را در پروژه‌های واقعی به یک معمای پیچیده تبدیل می‌کند.

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

تفاوت هدر و بدنه در عمل

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

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

Output Buffering و چرخه ارسال هدر

PHP یک مکانیزم به‌نام Output Buffering (بافر خروجی) دارد که به توسعه‌دهنده اجازه می‌دهد خروجی را در حافظه نگه دارد و بعداً ارسال کند. این مکانیزم، در بعضی سناریوها می‌تواند خطای مورد بحث را پنهان کند ولی در بلندمدت، استفاده نادرست از آن، مسائل جدیدی ایجاد می‌کند.

Output Buffering چطور کار می‌کند؟

به‌طور پیش‌فرض، هر بار که PHP دستور echo یا print را اجرا می‌کند، داده مستقیماً به مرورگر فرستاده می‌شود. با فعال‌سازی Output Buffering، داده‌ها در یک بافر موقت در حافظه سرور نگه داشته می‌شوند و فقط در پایان اجرای اسکریپت یا هنگام پر شدن بافر، به مرورگر فرستاده می‌شوند. این یعنی تا زمانی که بافر پر نشده، می‌توان هدر جدید اضافه کرد.

ob_start();

echo 'Some output';

// این هنوز کار می‌کند چون خروجی در بافر است
header( 'Location: /some-page/' );

ob_end_flush();

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

Output Buffering در وردپرس

در وردپرس، Output Buffering به‌طور پیش‌فرض استفاده نمی‌شود ولی بعضی افزونه‌ها و بعضی هاست‌ها آن را فعال می‌کنند. اگر در تنظیمات php.ini مقدار output_buffering روی 4096 یا On باشد، بافر به‌طور خودکار فعال می‌شود. این تنظیم، در بعضی سناریوها مفید است ولی در بعضی دیگر، خطاهای پیچیده‌ای ایجاد می‌کند که تشخیص‌شان سخت است چون رفتار سرور با رفتار محیط تست فرق می‌کند.

توصیه میدانی من این است که در تنظیمات هاست، مقدار output_buffering را روی Off بگذارید و در کد، فقط در جاهای بسیار مشخص از بافر استفاده کنید. این رویکرد، شفافیت کد را بالا می‌برد و از رفتارهای غیرمنتظره جلوگیری می‌کند. اصول دقیق کد امن و شفاف در نوشتن کد PHP امن برای وردپرس آمده است.

Output Buffering در نگاه اول راه‌حل جادویی خطای headers already sent است، ولی در بلندمدت، استفاده نادرست از آن هزینه‌ای بیش از فایده دارد.

نه علت ریشه‌ای در پروژه‌های وردپرسی

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

علت اول: فضای خالی قبل از تگ باز PHP

شایع‌ترین علت، و در عین حال سخت‌ترین برای تشخیص. اگر در ابتدای فایل PHP، یک فضای خالی یا newline قبل از <?php وجود داشته باشد، PHP آن را به‌عنوان خروجی ارسال می‌کند و پنجره ارسال هدر را می‌بندد:

 

این فضای خالی، در ویرایشگر متن معمولاً دیده نمی‌شود چون به‌عنوان بخشی از فایل محسوب می‌شود. برای تشخیص، باید از ابزارهایی استفاده کنید که کاراکترهای نامرئی را نمایش می‌دهند. در VS Code، فعال‌سازی گزینه «Render Whitespace» این مشکل را آشکار می‌کند.

علت دوم: BOM (Byte Order Mark) در ابتدای فایل

BOM یک کاراکتر نامرئی است که بعضی ویرایشگرها خصوصاً در ویندوز، به‌طور خودکار به ابتدای فایل‌های UTF-8 اضافه می‌کنند. این کاراکتر سه بایتی، در ظاهر بخشی از فایل نیست ولی PHP آن را به‌عنوان خروجی ارسال می‌کند و خطای مورد بحث را برمی‌گرداند. مفهوم BOM در مرجع فنی وب با عنوان Byte order mark شناخته می‌شود.

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

grep -rl $'\xEF\xBB\xBF' wp-content/plugins/

اگر فایلی دارای BOM باشد، این دستور آن را نشان می‌دهد. راه‌حل، استفاده از ویرایشگرهایی است که امکان ذخیره‌سازی بدون BOM را فراهم می‌کنند. در VS Code، این تنظیم در پایین صفحه ویرایشگر قابل انتخاب است.

علت سوم: خط خالی بعد از تگ بسته PHP

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

علت چهارم: echo یا print قبل از هدر

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

// اشتباه
echo 'Processing...';
header( 'Location: /dashboard/' );

// درست
header( 'Location: /dashboard/' );
exit;

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

علت پنجم: include فایلی که خروجی تولید می‌کند

اگر فایلی که با include یا require بارگذاری می‌شود، خودش خروجی تولید کند، همان خروجی باعث خطا می‌شود. این سناریو در پروژه‌های بزرگ که فایل‌ها به‌طور زنجیره‌ای include می‌شوند، شایع است. ریشه مشکل، در یکی از فایل‌های انتهای زنجیره است ولی خطا در فایلی که هدر ارسال می‌کند گزارش می‌شود.

علت ششم: استفاده اشتباه از هوک‌های وردپرس

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

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

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

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

علت هشتم: setcookie بعد از خروجی

مشابه session_start، تابع setcookie() هم یک هدر ارسال می‌کند و باید قبل از هر خروجی فراخوانی شود. در وردپرس، توابع سطح بالاتری مثل wp_set_auth_cookie و set_transient هم ممکن است در نهایت به setcookie برسند. هر جای کد که از این توابع استفاده می‌کنید، باید مطمئن شوید که قبل از شروع خروجی اجرا می‌شوند.

علت نهم: redirect در فایل functions.php قالب

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

add_action( 'template_redirect', function () {
    if ( ! is_user_logged_in() && is_page( 'dashboard' ) ) {
        wp_safe_redirect( wp_login_url() );
        exit;
    }
} );

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

در میان این نه علت، سه علت اول (فضای خالی، BOM و خط خالی بعد از ?>) بیشترین سهم را در پروژه‌های واقعی دارند. اگر فقط این سه را در پروژه خود بررسی کنید، احتمالاً در ۷۰ درصد موارد به علت اصلی می‌رسید.

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

مرحله تشخیص: از لاگ تا ابزار حرفه‌ای

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

ابزار اول: WP_DEBUG و debug.log

اولین قدم، فعال‌سازی حالت دیباگ در wp-config.php است:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

با این تنظیمات، پیام‌های خطا در فایل wp-content/debug.log ثبت می‌شوند. پیام headers already sent معمولاً دو فایل را نشان می‌دهد: یکی فایل مقصر که خروجی از آن شروع شده و یکی فایل ثانویه که در آن تلاش برای ارسال هدر انجام شده. تمرکز عیب‌یابی باید روی فایل مقصر باشد. اصول دقیق در تست و دیباگ پروژه‌های توسعه وردپرس آمده است.

ابزار دوم: بررسی فایل با ابزارهای حرفه‌ای

در فایل‌های PHP، کاراکترهای نامرئی مثل BOM و whitespace می‌توانند خطا ایجاد کنند. برای تشخیص دقیق، از ابزارهای زیر استفاده می‌کنم:

  • در VS Code، با فعال‌سازی Render Whitespace، فضای خالی و tab نمایش داده می‌شوند.
  • در vim، با دستور :set list، کاراکترهای نامرئی نمایش داده می‌شوند.
  • برای BOM، از دستور file --mime-encoding در لینوکس استفاده می‌کنم که فایل‌های UTF-8 with BOM را تشخیص می‌دهد.

یک ابزار دیگر که در پروژه‌های بزرگ زیاد به کارم آمده، اسکریپت Python زیر است که به‌طور سریع همه فایل‌های یک پوشه را بررسی می‌کند:

import os

for root, dirs, files in os.walk( 'wp-content/plugins/' ):
    for file in files:
        if file.endswith( '.php' ):
            path = os.path.join( root, file )
            with open( path, 'rb' ) as f:
                content = f.read()
                if content.startswith( b'\xEF\xBB\xBF' ):
                    print( f'BOM detected: {path}' )

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

ابزار سوم: Xdebug برای تحلیل عمیق

در پروژه‌های پیچیده که فایل‌های زیادی درگیر هستند، Xdebug ابزار قابل اتکایی است. با Xdebug می‌توانید در IDE با یک breakpoint، دقیقاً ببینید که خروجی از کجا شروع شده. این رویکرد، در پروژه‌های بزرگ که چندین لایه include وجود دارد، تفاوت محسوسی در زمان تشخیص می‌سازد.

ابزار چهارم: تست در محیط staging

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

نکته امنیتی در تشخیص

در سایت production، هرگز WP_DEBUG را برای مدت طولانی فعال نگه ندارید چون سه مشکل ایجاد می‌کند: اول، فایل debug.log می‌تواند به‌سرعت به چند گیگابایت برسد. دوم، اطلاعات حساس ممکن است در لاگ‌ها ثبت شوند. سوم، نمایش خطاها به کاربران، سطح امنیتی سایت را پایین می‌آورد چون مسیر فایل‌ها و ساختار کد را افشا می‌کند.

الگوهای رفع برای هر علت

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

رفع برای فضای خالی و BOM

الگوی استاندارد، حذف فضای خالی و BOM از ابتدای فایل است. در ویرایشگر VS Code، گزینه Save with Encoding و انتخاب UTF-8 بدون BOM این کار را انجام می‌دهد. برای اطمینان، پس از ذخیره‌سازی، با ابزارهایی که در بخش تشخیص توضیح دادم، بررسی کنید که فایل بدون BOM ذخیره شده است.

الگوی درست فایل PHP در وردپرس:

<?php
/**
 * Plugin Name: My Plugin
 * Description: A description.
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

// ادامه کد بدون تگ بسته

دقت کنید که فایل با <?php شروع می‌شود، نه با فضای خالی، نه با newline، نه با BOM. و در انتهای فایل، تگ ?> نوشته نشده است.

رفع برای echo قبل از هدر

الگوی درست، انتقال echo بعد از header یا حذف echo است:

function myplugin_handle_login() {
    $user = wp_authenticate( $username, $password );

    if ( is_wp_error( $user ) ) {
        return;
    }

    wp_set_auth_cookie( $user->ID );
    wp_safe_redirect( home_url( '/dashboard/' ) );
    exit;
}
add_action( 'admin_post_nopriv_myplugin_login', 'myplugin_handle_login' );

در این الگو، هیچ echo یا print قبل از wp_safe_redirect وجود ندارد و در پایان هم exit فراخوانی شده تا کد بعدی اجرا نشود. اصول دقیق در نوشتن کد PHP امن برای وردپرس آمده است.

رفع برای هوک‌های اشتباه

الگوی درست، استفاده از هوک‌های مناسب برای عملیات مختلف است:

// برای redirect و هدر در front-end
add_action( 'template_redirect', 'myplugin_check_access' );

// برای عملیات در admin
add_action( 'admin_init', 'myplugin_admin_check' );

// برای پردازش فرم‌ها
add_action( 'admin_post_myplugin_action', 'myplugin_handle_action' );
add_action( 'admin_post_nopriv_myplugin_action', 'myplugin_handle_action' );

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

رفع برای include فایل تولیدکننده خروجی

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

<?php
// در ابتدای فایل include شده
defined( 'ABSPATH' ) || exit;

// کد بدون هیچ echo یا print در سطح فایل
function myplugin_helper() {
    // کد تابع
}
// بدون تگ بسته در انتهای فایل

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

رفع با Output Buffering به‌عنوان راه‌حل موقت

در بعضی سناریوها که رفع علت اصلی سریع امکان‌پذیر نیست، استفاده از Output Buffering راه‌حل موقت است. ولی این رویکرد، شرط‌هایی دارد:

if ( ! ob_get_level() ) {
    ob_start();
}

// کدی که ممکن است خروجی تولید کند

header( 'Location: /target/' );

if ( ob_get_level() ) {
    ob_end_clean();
}

exit;

سه نکته مهم در این الگو. اول، بررسی ob_get_level قبل از ob_start تا بافر تکراری ایجاد نشود. دوم، استفاده از ob_end_clean به‌جای ob_end_flush تا خروجی ناخواسته پاک شود. سوم، همیشه exit در پایان تا کد اضافه اجرا نشود. استفاده از این الگو باید استثنا باشد نه قاعده.

در میان این الگوهای رفع، الگوی اول (حذف فضای خالی و BOM) و الگوی دوم (انتقال echo بعد از هدر) بیشترین کاربرد را در پروژه‌های واقعی دارند. سه الگوی دیگر، برای سناریوهای خاص هستند که کمتر پیش می‌آیند ولی وقتی پیش می‌آیند، رفع بدون آن‌ها غیرممکن است.

زمینه‌های خاص وردپرس: redirect و session

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

زمینه اول: ریدایرکت در جریان ورود کاربر

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

function myplugin_custom_login_redirect( $redirect_to, $requested, $user ) {
    if ( ! is_wp_error( $user ) && isset( $user->roles ) ) {
        if ( in_array( 'administrator', $user->roles, true ) ) {
            return admin_url();
        }
    }

    return $redirect_to;
}
add_filter( 'login_redirect', 'myplugin_custom_login_redirect', 10, 3 );

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

زمینه دوم: session_start در افزونه‌های سفارشی

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

add_action( 'init', function () {
    if ( ! session_id() && ! headers_sent() ) {
        session_start();
    }
}, 1 );

سه نکته در این الگو. اول، استفاده از هوک init با اولویت ۱ که قبل از هر خروجی اجرا می‌شود. دوم، بررسی session_id که اگر جلسه قبلاً شروع شده باشد، دوباره شروع نشود. سوم، بررسی headers_sent که اگر هدر قبلاً ارسال شده باشد، تلاش نکنیم.

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

زمینه سوم: redirect در متاباکس‌ها

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

function myplugin_handle_settings_save() {
    if ( ! isset( $_POST['myplugin_nonce'] ) ) {
        return;
    }

    $nonce = sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) );

    if ( ! wp_verify_nonce( $nonce, 'myplugin_save_settings' ) ) {
        return;
    }

    // ذخیره تنظیمات
    update_option( 'myplugin_settings', $_POST );

    wp_safe_redirect( add_query_arg( 'updated', 'true', wp_get_referer() ) );
    exit;
}
add_action( 'admin_post_myplugin_save', 'myplugin_handle_settings_save' );

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

پیشگیری ساختاری در کد وردپرس

بهترین راه‌حل برای خطای headers already sent، پیشگیری است. در پروژه‌های واقعی، تجربه‌ام این است که اگر در پنج لایه پیشگیری انجام شود، این خطا تقریباً هرگز ظاهر نمی‌شود.

لایه اول: حذف تگ بسته PHP در همه فایل‌ها

در همه فایل‌های PHP در افزونه‌ها و قالب‌ها، تگ بسته ?> را در انتهای فایل نگذارید. این توصیه استاندارد وردپرس است و در بسیاری از پروژه‌های واقعی، این یک تغییر کوچک، خطاهای زیادی را حذف کرده است.

لایه دوم: تنظیم ویرایشگر برای ذخیره بدون BOM

در همه ویرایشگرهای مدرن، امکان ذخیره‌سازی بدون BOM وجود دارد. این تنظیم را در سطح پروژه فعال کنید تا همه اعضای تیم از آن استفاده کنند. در VS Code، این تنظیم در فایل .vscode/settings.json قابل اعمال است:

{
  "files.encoding": "utf8",
  "files.eol": "\n",
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": false
}

این تنظیمات، فضای خالی انتهای خطوط و newline انتهای فایل را حذف می‌کنند که هر دو می‌توانند منبع خطا باشند.

لایه سوم: استفاده از هوک‌های مناسب برای عملیات

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

عملیاتهوک مناسبمحدودیت
ریدایرکت front-endtemplate_redirectقبل از رندر صفحه
ریدایرکت adminadmin_initقبل از رندر صفحه ادمین
پردازش فرمadmin_post_{action}قبل از رندر پیشخوان
session_startinit با اولویت ۱قبل از هر خروجی
setcookieinit یا قبل از آنقبل از هر خروجی

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

لایه چهارم: بازبینی کد قبل از انتشار

قبل از هر انتشار افزونه، سه چیز را بررسی کنید. اول، همه فایل‌های PHP با تگ <?php شروع شوند بدون فاصله قبلی. دوم، همه فایل‌ها بدون BOM ذخیره شده باشند. سوم، در انتهای هیچ فایلی تگ ?> وجود نداشته باشد. این سه بررسی، در یک اسکریپت ساده قابل خودکارسازی است.

لایه پنجم: تست در محیط staging قبل از production

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

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

پیشگیری از خطای headers already sent، نه با یک تکنیک بلکه با پنج عادت کوچک محقق می‌شود؛ هرکدام به‌تنهایی کم‌اثر، ولی در کنار هم قوی.

پرسش‌های پرتکرار درباره خطای headers already sent

خطای headers already sent چه معنایی دارد؟ این خطا در PHP یعنی کد شما تلاش کرده یک هدر HTTP ارسال کند، ولی مرورگر قبلاً داده‌ای دریافت کرده است. مرورگر پس از دریافت اولین بایت داده، پنجره ارسال هدر را می‌بندد و هر تلاش بعدی برای ارسال هدر، خطا برمی‌گرداند. علت شایع این خطا، خروجی ارسال شده قبل از هدر است.

تفاوت این خطا با خطای Cannot modify header information چیست؟ این دو در واقع یک خطا هستند. پیام کامل خطا در بسیاری از نسخه‌های PHP به شکل «Cannot modify header information - headers already sent by» است. در بعضی مستندات، فقط بخش دوم پیام استفاده می‌شود. اگر با پیام کامل مواجه شدید، این راهنما برای شماست. اگر با نسخه کوتاه‌تر مواجه شدید، «خطای Cannot modify header information در PHP» راهنمای مکملی است.

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

آیا راه‌حل سریع وجود دارد؟ اگر بخواهید فقط خطا را از بین ببرید، استفاده از Output Buffering راه‌حل موقت است. ولی این راه‌حل، حافظه سرور را مصرف می‌کند و در بلندمدت هزینه دارد. راه‌حل ریشه‌ای، حذف خروجی قبل از هدر است نه بافر کردن خروجی.

چطور بفهمم فایل مقصر کدام است؟ پیام خطا دو فایل را نشان می‌دهد: یکی فایلی که خروجی از آن شروع شده (مقصر اصلی) و یکی فایلی که در آن تلاش برای ارسال هدر انجام شده. تمرکز عیب‌یابی باید روی فایل مقصر باشد نه فایل ثانویه. اگر پیام خطا در debug.log کامل نبود، از Xdebug استفاده کنید.

BOM چیست و چگونه آن را حذف کنم؟ BOM یا Byte Order Mark یک کاراکتر نامرئی است که بعضی ویرایشگرها خصوصاً در ویندوز به‌طور خودکار به ابتدای فایل‌های UTF-8 اضافه می‌کنند. برای حذف، فایل را در ویرایشگرهایی مثل VS Code باز کنید و با گزینه Save with Encoding، حالت UTF-8 (بدون BOM) را انتخاب کنید. برای تشخیص، از دستور grep -rl $'\xEF\xBB\xBF' در لینوکس استفاده کنید.

آیا خطای headers already sent روی سرعت سایت اثر دارد؟ در حالت Warning، تأثیر مستقیم روی سرعت کم است ولی به‌دلیل وقفه در پردازش، بار اضافه روی سرور ایجاد می‌کند. اگر این خطا باعث شکست ریدایرکت شود، ممکن است کاربر نتواند وارد شود که خودش یک افت جدی در تجربه کاربری است. اگر با گلوگاه‌های سرعت سایت آشنا نیستید، تاثیر هاست بر سرعت سایت چقدر است تحلیل دقیقی دارد.

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

چطور بفهمم کدام افزونه خطا را ایجاد می‌کند؟ روش استاندارد، غیرفعال کردن همه افزونه‌ها و فعال کردن آن‌ها یکی‌یکی است. با هر فعال‌سازی، صفحه را باز کنید و ببینید خطا رخ می‌دهد یا نه. اگر با آشنایی کمتری با این روش دارید، پیشنهاد می‌کنم نوشته «تست و دیباگ پروژه‌های توسعه وردپرس» را بخوانید که روش کامل را توضیح داده است.

چرا این خطا فقط در بعضی مرورگرها رخ می‌دهد؟ مرورگرهای مختلف رفتار متفاوتی با خروجی نامرئی مثل BOM دارند. بعضی مرورگرها BOM را نادیده می‌گیرند و بعضی آن را به‌عنوان محتوا رد می‌کنند. این تفاوت رفتاری، باعث می‌شود که این خطا در بعضی مرورگرها ظاهر شود و در بعضی دیگر نباشد.

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

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

چرا این خطا در قالب‌های سفارشی بیشتر دیده می‌شود؟ چون قالب‌های سفارشی معمولاً کدهای بیشتری در فایل functions.php دارند و بعضی از این کدها ممکن است خروجی تولید کنند. علاوه بر این، قالب‌های سفارشی ممکن است فایل‌های اضافی داشته باشند که BOM یا فضای خالی در ابتدای‌شان باشد.

آیا این خطا می‌تواند نشانه مشکل امنیتی باشد؟ به‌طور مستقیم نه، ولی در بعضی سناریوها نشانه مشکل امنیتی است. مثلاً اگر افزونه‌ای که این خطا را ایجاد می‌کند، کد ناامن داشته باشد، خودِ خطا در ظاهر بی‌خطر است ولی می‌تواند نشانه‌ای از وجود کد ناامن باشد. اصول دقیق امنیت در «راهنمای امنیت وردپرس برای مبتدیان» آمده است.

آیا این خطا با خطای Parse error در PHP یکی است؟ نه، این دو خطا متفاوتند. خطای Parse error وقتی رخ می‌دهد که سینتکس PHP نادرست باشد. خطای headers already sent وقتی رخ می‌دهد که ترتیب ارسال هدر و خروجی نقض شود. اگر با خطای اول مواجه هستید، خطای Parse error در PHP چیست و چگونه رفع می‌شود؟ راهنمای مکملی است.

آیا این خطا در PHP 8 بیشتر از PHP 7.4 رخ می‌دهد؟ فرکانس خطا تغییری نکرده، ولی پیام آن در PHP 8 دقیق‌تر است. PHP 8 فایل و خط دقیقی که خروجی از آن شروع شده را گزارش می‌کند که عیب‌یابی را ساده‌تر می‌کند.

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

آیا استفاده از تگ بسته ?> در فایل‌های PHP همیشه بد است؟ نه در همه جا، ولی در فایل‌های PHP که فقط کد دارند و در انتهای‌شان چیز دیگری نیست، حذف تگ بسته توصیه استاندارد وردپرس است. در فایل‌هایی که ترکیبی از PHP و HTML هستند، تگ بسته لازم است.

آیا این خطا با Restore Defaults در htaccess حل می‌شود؟ نه، این خطا در سطح PHP است نه در سطح سرور. راه‌حل، اصلاح کد است نه تغییر فایل htaccess.

از رفع موضعی به پیشگیری ساختاری

در پایان این مسیر، یک حقیقت را باید پذیرفت: خطای headers already sent، یک خطای ساده نیست؛ نشانه‌ای از یک الگوی ناهماهنگ در ترتیب اجرای کد است. اگر این خطا را فقط با Output Buffering پنهان کنید، ریشه مشکل همچنان باقی می‌ماند و در آینده، در سناریوهای پیچیده‌تر، دوباره ظاهر می‌شود. راه‌حل بلندمدت، پیشگیری ساختاری و رعایت استانداردهای وردپرس در نوشتن فایل‌های PHP است.

سه اصل که در همه پروژه‌های خودم رعایت می‌کنم. اصل اول: هرگز تگ بسته ?> را در انتهای فایل PHP ننویسید. اصل دوم: همیشه فایل‌ها را بدون BOM و بدون فضای خالی ابتدایی ذخیره کنید. اصل سوم: عملیات ارسال هدر را در هوک‌های مناسب مثل init، template_redirect و admin_post اجرا کنید. این سه اصل، در بلندمدت، این خطا را تقریباً حذف می‌کنند.

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، در یک نصب تستی وردپرس، یک افزونه ساده با BOM ایجاد کنید و ببینید این خطا چطور رخ می‌دهد و چطور رفع می‌شود. دوم، در یک پروژه واقعی، همه فایل‌های PHP را با ابزارهایی که در این نوشته معرفی کردم بررسی کنید و ببینید چند فایل آسیب‌پذیر دارید. سوم، در کد افزونه خود، همه تگ‌های بسته ?> را حذف کنید و ببینید چطور خوانایی و پایداری کد بالا می‌رود. این سه تجربه، درک عمیقی از اهمیت این خطا به شما می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود. 🛠️