وبلاگ شخصی امیر عماد محمودپور

برنامه نویس تحت وب

وبلاگ شخصی امیر عماد محمودپور

برنامه نویس تحت وب

پایگاه آموزش رایگان برنامه نویسی

حل و بررسی جامع و تخصصی خطای headers already sent

پنجشنبه, ۴ تیر ۱۳۹۴، ۰۹:۴۰ ب.ظ

به نام خدا 


already-header-sent


اگر برنامه نویس PHP هستید یا به هر نحوی با PHP سر و کار دارید حتما با خطای headers already sent مواجه شده اید . این پیغام طبق آمار منتشر شده ، یکی از رایج ترین خطا هایی هست که برنامه نویسان با آن سر و کار دارند . در این مقاله به برسی ریشه ای این خطا میپردازیم . 



مهم ترین نکته ، عدم وجود هیچ گونه خروجی پیش از ارسال هدرها میباشد 


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


Warning: Cannot modify header information - headers already sent (output started at script:line)

مفهوم این اخطار به شرح زیر می باشد :
اخطار : سیستم قادر به تغییر اطلاعات هدر نبوده و هدرهای پیش از این ارسال شده اند.

در ادامه برخی توابع که هدر HTTP را دست خوش تغییراتی قرار می دهند به شرح زیر معرفی شده اند :

خروجی حاصله می تواند به دو شکل زیر متصور شود :

Unintentional (غیرعمدی) :

  • فضای خالی قبل از الگوی <?php یا بعد از ?>
  • UTF-8 Byte Order Mark به صورت صریح و واضح 
  • پیغام های پیشین خطا یا اخطارها
Intentional (عمدی) :

  • توابع print،  echo و دیگر توابعی که خروجی را تولید می کنند.
  • بخش های خام(Raw) <html> پیش از کد <?php

حال اجازه دهید بررسی کنیم که چرا این حالت اتفاق می افتد. ادامه این توضیحات را از با دقت بررسی کنید.
به منظور درک کامل از اینکه چرا هدرها پیش از خروجی باید ارسال شوند لازم است تا نگاهی به یک واکنش متداول HTTP داشته باشید. اسکریپت های PHP اساساً  محتویات HTML را ایجاد می کنند اما همیشه به یاد داشته باشید که این اسکریپتها وظیفه ارسال یک مجموعه از هدرهای HTTP/CGI تحت وب سرور را نیز بر عهده دارند.

HTTP/1.1 200 OK Powered-By: PHP/5.3.7 Vary: Accept-Encoding Content-Type: text/html; charset=utf-8 <title>PHP page output page</title> <h1>Content</h1>

Some more output follows...</p> and <a href="/"> <img src="internal-icon-delayed"> </a>



صفحه خروجی ما همیشه پیرو قوائد تعیین شده توسط هدرها هستند.
برای این منظور ساختار PHP به گونه ای تنظیم شده تا پیش از انجام کارهای دیگر باید هدرها را به وب سرور منتقل کند.
البته این فرایند تنها می تواند برای یک مرتبه تکرار شود. چرا که بعد از یک double linebreak، دیگر هرگز نمی تواند به تجدید قوای آنها بپردازد. 
زمانی که PHP اولین خروجی های خود را توسط دستوراتی همچون (print, echo, <html>) دریافت می کند، نسبت به تهی کردن تمامی هدرهای جمع آوری شده اقدام می کند.سپس بعد از انجام این کارها می تواند تمامی خروجی های مد نظر خود را ارسال کند. اما از این به بعد ارسال هدرهای دیگر HTTP امری غیر ممکن خواهد بود.
حال اجازه دهید ببینیم این خروجی نابه هنگام در چه مکانی رخ داده است. ادامه توضیحات را با دقت زیر نظر بگیرید.
در واقع کلید این معما در اختیار اخطار header() قرار گرفته است. در واقع این اخطار دربر دارنده تمامی اطلاعات مرتبط بوده که می توان از آنها به منظور بررسی علت خطا استفاده کرد. به اخطار حاصله خوب دقت کنید :

Warning: Cannot modify header information - headers already sent by (output started at /www/usr2345/htdocs/auth.php:52) in /www/usr2345/htdocs/index.php on line 100


در واقع خط 100 از این مجموعه دستورات به محلی از اسکریپت اشاره می کند که فراخوانی header() با شکست مواجه شده است. بخش "output started at" که به صورت برجسته در متن اخطار مشخص شده است رهگشا ترین بخش موجود برای رسیدن به جواب این مسئله است. این بخش در واقع محل و منبع خروجی قبلی را به ما نشان می دهد. در این مثال، فایل مشخص شده auth.php بوده و شماره خطی که در آن مشکل وجود دارد در اینجا 52 گزارش شده است. این مکان در واقع همان نقطه ای است که شما پیش از این باید برای خروجی نابه هنگام مورد بررسی قرار می دادید.


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

  1. Print, Echo 

در واقع خروجی های عمدی حاصله از دستورات print و echo فرصت ارسال هدرهای HTTP را از بین می برد. برای جلوگیری از این مشکل باید جریان و ساختار شکل گیری برنامه را از نو دوباره بازسازی کنیم. برای این منظور از functions ها و الگوهای نمونه بهره ببرید. همچنین مطمئن شوید که فراخوانی های header() پیش از اینکه پیغام ها نوشته شوند رخ دهند. به طور کلی از توابعی که می توان به منظور تولید خروجی استفاده کرد می توان موارد زیر را نام برد :

  • print, echo, printf, vprintf
  • trigger_error, ob_flush, ob_end_flush, var_dump, print_r
  • readfile, passthru, flush, imagepng, imagejpeg


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


2- نواحی خام HTML :

نکته ای که باید به آن توجه داشته باشید این است که بخش های تبدیل نشده در یک فایل .php به عنوان خروجی مستقیم نیز مورد استفاده قرار می گیرند. شرایط یک اسکرپیت که وظیفه اجرای فراخوانی یک هدر را بر عهده دارد باید پیش از هر نوع بلوک خام <HTML> مورد بررسی قرار بگیرد.

<!DOCTYPE html> <?php // Too late for headers already.


شما می توانید از یک الگوی نمونه به منظر جداسازی پردازش از منطق خروجی (output logic) به صورت زیر بهره ببرید :

  • کد form processing را در بالای اسکرپیت ها قرار دهید.
  • از متغیرهای رشته ای موقتی به منظور به تعویق انداختن پیغام ها استفاده کنید.
  • منطق واقعی خروجی و خروجی ترکیب شده HTML باید از آخرین بخش پیروی کنند.

3- فضای خالی قبل از <?php برای اخطارهای مربوط به Script.php line 1 :

به طور کلی اگر اخطار به خروجی حاصله از خط اول اشاره می کند، نتیجه حاصله اغلب منجر به تولید فضای خالی، متن یا HTML پیش از باز کردن تگ <?php خواهد شد.

<?php # There's a SINGLE space/newline before <? - Which already seals it.


همچنین به طور مشابهی این حالت می تواند برای اسکرپیت های اضافه شده و بخش های اسکریپت به مانند زیر رخ دهد :

?> <?php

ساختار PHP به گونه ای است که بعد از تگهای بسته یک linebreak واحد را حذف می کند. اما با این وجود از چندین خط جدید، زبانه یا فضاهایی که در بخش های درونی قرار گرفته اند صرف نظر نخواهد کرد.


4- UTF-8 BOM : 

جالب است بدانید که لاینبریک ها و فضاهای خالی خود به تنهایی می توانند مشکلاتی را ایجاد کنند.
اما همچنین در بین کاراکترهای موجود دنباله هایی از کاراکتر نامرئی وجود دارد که می توانند علت این مشکل باشند.
در این بین ساختار UTF-8 BOM (Byte-Order-Mark) نیز وجود دارد که در اغلب ویرایشگرهای متنی نمایش داده نمی شود.
در واقع آن یک دنباله بایتی به ترتیب EF BB BF بوده که البته برای سندهایی که تحت استاندارد UTF-8 کدگذاری می شوند اختیاری و اضافی هستند.
به هر حال PHP باید با آن به عنوان یک خروجی خام رفتار کند. این حالت ممکن است به عنوان کاراکترهای  در خروجی نمایش داده شود البته این وضعیت در شرایطی اتفاق می افتاد که کلاینت سند را به عنوان استاندارد Latin-1 تفسیر کند. حالت دیگر این است که با آن به عنوان یک garbage برخورد می کند.
در ویرایشگرهای گرافیکی خاص و IDEهای مبتنی بر زبان جاوا نسبت به وجود آنها بی تفاوتی خاصی دیده می شود. در واقع با آنها چنان رفتار می شود که گویی وجود ندارند. ناگفته نماند که این حالت توسط استاندارد یونیکد ارائه شده است. در هر صورت به طور کلی اغلب برنامه نویسان و ویرایشگران کنسولی از فرایندهایی که در تصویر زیر مشخص شده، بهره می گیرند :

invalid

در این فرایند تشخیص نوع مشکل به وجود آمده به آسانی قابل تعیین است. شاید بتوانید در دیگر ویرایشگرها هویت آن را تحت منوی file/settings شناسایی کنید. ناگفته نماند که با استفاده از Notepad++ تحت ویندوز نیز می توانید مشکل را شناسایی و نسبت به رفع آن اقدام کنید. راه حل دیگری به منظور شناسایی هویت BOMها وجود دارد و آن بازمرتب سازی تحت یک hexeditor است. تحت سیستم های *nix، مقدار hexdump معمولا قابل دسترسی است در غیر این صورت یک مغایرت گرافیکی فرایند بازبینی را تجزیه و ساده می کند. به تصویر زیر دقت کنید :

hexdump

یک روش ساده به منظور حل کردن مشکل به وجود آمده در واقع این است که ویرایشگر متنی را طوری تنظیم کنید که فایل ها را تحت استاندارد "UTF-8 (no BOM)" یا تحت دیگر ساختارهای مشابه ذخیره کند. اغلب افراد تازه کار طور دیگر عمل می کنند مثلا آنها فرایند بازمرتب سازی را اعمال می کنند تا فایل های جدید ایجاد شده و در آخر تنها کد قبلی را در جالی فعلی کپی می کنند.


ابزارهای کاربردی تصحیح سازی :

در حال حاضر چندین نرم افزار اتومات به منظور تجزیه تحلیل و بازنویسی فایل های متنی (sed/awk یا recode) وجود دارد. به طور ویژه برای PHP نیز می توانید از ابزارهای phptags tag tidier بهره ببرید. با این ابزارها می توانید تگهای باز و بسته را در داخل حالات بلند و کوتاه بازنویسی کنید. استفاده از این ابزارها همچنین سبب اصلاح فضای خالی برجسته و دنباله ای و حل مشکلات UTF-x BOM و یونیکد خواهد شد. به الگوی زیر دقت کنید :


phptags --whitespace *.php

بهترین روش این است که از این الگو روی یک دستور include کامل یا تحت پوشه پروژه بهره ببرید.

5- فضای خالی بعد از ?> :

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

به طور کلی مخصوصاً به افراد تازه کار توصیه می شود که تگهای بسته کشیده ?> در PHP را باید حذف کنند. این راهکار سبب می شود تا از بروز یک سری مشکلات جلوگیری به عمل آید. در این حالت معمولا اسکریپت های include()d منشا اصلی چنین مشکلاتی هستند.

6- منبع خطا به صورت Unknown on line 0 :

امروزه آنقدر گستردگی خطاها زیاد شده که حتی اگر در تنظیم PHP extension یا php.ini نیز اشتباهی صورت بگیرد و منبع خطا نیز نامعلوم باشد امری عادی است.

  • گاهی اوقات این حالت می توانید یک تنظیم رمزنگاری gzip stream بوده و یا یک  ob_gzhandler باشد. 
  • گاهی اوقات نیز این حالت می تواند یک ماژول extension= دوبار بارگذاری شده ای باشد که یک پیغام اخطار یا شروع به کار مشخص PHP تولید می کند.
7- پیغام های خطای اولیه :

اگر عبارت یا دستور PHPی دیگری سبب ایجاد پیغام خطا یا اخطار نمایان شده ای شد، آنرا به عنوان یک خروجی زودرس محاسبه خواهد کرد. در این مورد شما لازم است تا از این خطا صرف نظر کرده،اجرای دستورات را به تعویق انداخته یا ییغام را زمانی که از خطایابی های بعدی جلوگیری نمی شود با الگوهایی همچون isset() یا @() مسدود سازید.

عدم پیغام خطا :

اگر شما الگوهای error_reporting یا display_errors را در فایل php.ini غیرفعال کرده باشید هیچ پیغام اخطاری نمایش داده نخواهد شد. اما به هر حال صرف نظر کردن از خطاها صورت مسئله را پاک نمی کند. هنوز هدرها بعد از خروجی زودرس قادر به ارسال نیستند.
بنابراین زمانی که ریدارکت های header("Location: ...") به سادگی با شکست روبرو می شود بسیار توصیه می شود تا به بررسی و شناسایی اخطارها بپردازید.
برای بررسی و برطرف کردن خطاها لازم است تا آنها را با دو دستور ساده در اسکرپیت فراخوان دوباره فعال سازی کنید. به مانند زیر :

error_reporting(E_ALL); ini_set("display_errors", 1);

همچنین می توانید در صورتی که باقی موارد با شکست روبرو شد از دستور زیر بهره ببرید :

set_error_handler("var_dump");

در هنگام مواجهه با هدرهای ریدایرکت، شما باید اغلب از یک الگو به مانند زیر برای مسیرهای نهایی کد استفاده کنید :

exit(header("Location: /finished.html"));

ترجیحا بهتر است اگر مایل بودید از یک تابع مفید به منظور چاپ پیغام کاربری در مورد خطاهای header() استفاده کنید.


Output buffering به عنوان یک workaround :



Output buffering در ساختار PHP یک روش مخصوص است که به منظور سبک کردن بار این مشکل مورد استفاده قرار می گیرد.  این روش اغلب اوقات کار خود را به درستی انجام می دهد. اما به هر حال نباید استفاده از آن را با  ساختار مناسب برنامه و  جداکردن خروجی از روند منطقی برنامه یا همان  control logic  معامله کرد. هدف واقعی این روش در واقع کاهش نقل و انتقالات مکرر به وب سرور است.


  1. استفاده از  output_buffering= می تواند راه گشا و مفید واقع شود. شما می توانید آنرا از طریق فایل php.ini، .htaccess یا حتی از طریق .user.ini تحت تنظیمات پیشرفته FPM/FastCGI پیکربندی کنید. فعال سازی آن به PHP این امکان را خواهد داد تا به جای انتقال یکباره خروجی به وب سرور، عمل بافرینگ را روی آن انجام دهد. در این صورت PHP می تواند هدرهای HTTP را در یکجا جمع کند.
  2. این فرایند همچنین می تواند با یک فراخوانی به ob_start(); در بالای اسکرپیت فراخوان به کار گرفته شود. البته این حالت بنا به دلایلی که در زیر به آن اشاره شده است اطمینان ار حصول درست کار را می کاهد :
  • حتی اگر دستور <?php ob_start(); ?>، کلید اجرای اولین اسکریپت باشد، فضای خالی یا یک BOM ممکن است پیش از این دگرگون شده و نتیجه حاصله نیز یک رندرینگ نامناسب را رقم خواهد زد.
  • حالت دیگر در صورت استفاده از این روش این است که می تواند فضای خالی را تحت خروجی HTML پنهان کند. اما به محض اینکه منطق برنامه سعی می کند محتوای باینری را(برای مثال یک تصویر) را ارسال کند، خروجی نامرتبط بافر شده، خود یک مشکل را ایجاد می کند. که در نهایت اینجا مجبور می شویم از دستور ob_clean() بهره ببریم.
  • از نظر حجم و اندازه، بافر نیز محدودیت هایی دارد و هنگامی که تحت پیشفرض های موجود تنظیم شده باشد به سادگی لبریز شده و جایی برای دیگر داده ها نخواهد داشت. البته دفعات رخ دادن این حالت کم نبوده، و بررسی و رفع این مشکل نیز می توند کاری دشوار باشد.
به طور کلی نتیجه ای که می توان گرفت این است که هر دو روش امکان دارد از جهاتی مشکل ساز و به طور صد درصد قابل اعتماد نباشند. این مشکلات مخصوصاً زمانی که به production serverها یا به الگوهای توسعه سوییچ می کنید می توانند مشکلات بیشتری را به وجود آورند. در حقیقت به همین دلیل است که از فرایند output buffering به طور گسترده ای تنها به عنوان یک گزینه فرعی برای حل مشکلات استفاده می شود.


ولی روی سرور دیگ کار میکند ! 


شاید با خود بگویید که این روش روی سرور دیگر کار کرد. به طور کلی اگر شما پیش از این اخطارهای مربوط به هدرها را دریافت نکرده اید، در این حالت output buffering php.ini setting دیگر تغییر کرده است. علت این امر احتمالا این است که روی سرور فعلی یا جدید شما پیکربندی نشده است.


بررسی با دستور headers_sent() :


در صورتی که هنوز امکان ارسال هدرها وجود داشته باشد،  شما می توانید همیشه  از دستور headers_sent()  برای بررسی بیشتر استفاده کنید. این یک روش مفید است که می تواند بر اساس شروط خاصی توضیحاتی را چاپ کرده یا fallback logic دیگری را به کار ببرد. به دستورات زیر دقت کنید :


if (headers_sent()) { die("Redirect failed. Please click on this link: <a href=...>"); } else{ exit(header("Location: /user.php")); }

از روشهای مفید برگشتی(fallback) می توان به موارد زیر اشاره کرد :
  •  HTML<meta> tag:
اگر برنامه شما از نظر ساختاری به گونه ای است که اصلاح و ترمیم آن مشقت زیادی را می طلبد، می توانید از یک روش آسان اما تا حدی غیر حرفه ای استفاده کرده تا به ریدایرکت ها ، این امکان را بدهید تا یک تگ HTML <meta> را معرفی کنند. یک ریدایرکت می تواند از طریق الگوی زیر حاصل شود :

<meta http-equiv="Location" content="http://example.com/">

در صورتی که تمایل به ایجاد یک تاخیر کوتاه داشتید نیز می توانید از الگوی زیر بهره ببرید :

<meta http-equiv="Refresh" content="2; url=../target.html">

نتیجه ای که حاصل می شود این است که به هنگامی که مورد استفاده ما بخش <head> را رد می کند  منجر به ایجاد یک HTML نامتعبر خواهد شد.
  • JavaScript redirect :
به عنوان یک روش جایگزین یک JavaScript redirect می تواند تحت ریدارکت های صفحه مورد استفاده قرار بگیرد. به الگوی زیر دقت کنید :

<script> location.replace("target.html"); </script>

با وجود اینکه این حالت اغلب نسبت به روش <meta> بیشتر به ساختار باورپذیری HTML نزدیک است، اما به هر حال سبب ایجاد یک حس اطمینان تحت کلاینت هایی که از جاوا اسکریپت بهره می برند خواهد شد.
به هر حال از هر روشی که استفاده کنید، در نهایت زمانی که فراخوانی های واقعی HTTP header() با شکست مواجه شود،  fallback های قابل قبولی را ایجاد خواهد کرد.
همچنین مطلوب است که هر یک از این روش ها را با یک پیغام کاربر پسند و یک لینک قابل کلیک به عنوان آخرین اقدام بازمرتب سازی مجهز کنید. این حالت در واقع همان چیزی است که افزونه PECL http_redirect() انجام می دهد.

حال اجازه دهید ببینیم که چرا دستورات setcookie() و session_start() نیز تحت تاثیر قرار می گیرند.
نکته ای که باید به آن توجه داشته باشید این است که هر دو دوستور setcookie() و session_start() باید یک هدر HTTP تحت Set-Cookie: را ارسال کنند. با این وجود همان شرایط اینجا هم صدق می کند و موجب خواهد شد تا پیغام های خطا برای وضعیت های خروجی زودرس نمایان شوند.
البته ناگفته نماند که آنها بیشتر تحت کوکی های غیرفعال موجود در مرورگر تاثیر می پذریند یا حتی این آسیب پذیری می تواند مربوط به مسائل و مشکلات پروکسی نیز باشد. به طور کلی همچنین کارکرد ظاهری یک Session به فضای آزاد دیسک، تنظیمات دیگر فایل php.ini و دیگر موارد بستگی دارد.


 در پایان امیدوارم این توضیحات برای شما مفید واقع شده باشد.
ارادتمند : امیر عماد محمودپور 


نظرات (۱)

خیلی مطلب مفیدی بود .
ممنون
موفق و سربلند باشید
پاسخ:
قربان شما سعید جان 

محبت دارید . 

ارسال نظر

ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی