Giriş formunu doldurup gönderiyorsunuz. Kod doğru çalışıyor, kullanıcı bulunuyor, oturum açılıyor ve yönlendirme satırı da yerinde duruyor. Ama tarayıcı hiçbir yere gitmiyor; onun yerine sayfanın en üstünde kırmızı bir satır beliriyor:
Warning: Cannot modify header information - headers already sent by
(output started at /home/kullanici/public_html/wp-config.php:1)
in /home/kullanici/public_html/wp-includes/pluggable.php on line 1435
İnternette bu hatayı arattığınızda karşınıza çıkan cevapların çoğu tek bir öneriye indirgenir: dosyanın başına ob_start() ekleyin. Bu öneri hatayı gerçekten ortadan kaldırır, ama bir sebebi düzeltmez; sadece görünmez yapar. Birkaç hafta sonra aynı hata farklı bir dosyada, bu kez tarayıcıya yarım yüklenmiş bir sayfa olarak geri gelir.
Bu hata bir kaynak limiti sorunu değildir. Bellek yetersizliği ya da çalışma süresi aşımı gibi hatalarla hiçbir akrabalığı yoktur; sunucunuz yeterince güçlüdür, PHP sürümünüz doğrudur. Bu bir çıktı sırası hatasıdır ve iyi haber şu: hata mesajının kendisi suçluyu dosya ve satır numarasıyla birlikte size zaten söylüyor. Bu yazıda önce o mesajı okumayı, sonra beş temel nedeni sırayla elemeyi ve son olarak neyin gerçek çözüm neyin pansuman olduğunu ayırt etmeyi göreceksiniz.
Hata Mesajı Aslında Size Suçluyu Söylüyor#
Mesajın en kritik özelliği, içinde iki ayrı dosya ve satır numarası barındırmasıdır. Neredeyse herkes yanlış olana bakar.
| Mesaj parçası | Ne anlatır | Suçlu mu? |
|---|---|---|
in .../pluggable.php on line 1435 | header() veya setcookie() çağrısının yapıldığı yer | Hayır, burası mağdur |
(output started at .../wp-config.php:1) | Tarayıcıya ilk baytın gönderildiği yer | Evet, suçlu burası |
Yukarıdaki örnekte WordPress'in çekirdek dosyası olan pluggable.php hiçbir şey yapmamıştır; sadece yönlendirmeyi denemiştir. Asıl olay wp-config.php dosyasının 1. satırında olmuştur. Birinci satır, PHP açılış etiketinin bulunması gereken satırdır; oradan çıktı gelmesi neredeyse her zaman görünmez bir karakter anlamına gelir.
Bu okuma alışkanlığı kazandığınızda hata ayıklamanın yüzde doksanı biter. Bakacağınız yer her zaman parantez içindeki output started at kısmıdır. Satır numarası da doğrudan bilgi verir:
- Satır 1: açılış etiketinden önce boşluk ya da UTF-8 BOM var.
- Dosyanın son satırları: kapanış etiketinden sonra fazladan boşluk ya da boş satır kalmış.
- Ortalarda bir yer: orada gerçek bir
echo,print_r,var_dumpya da bastırılmamış bir PHP uyarısı var.
Neden Oluyor? HTTP Yanıtında Başlıklar Gövdeden Önce Gider#
Bir HTTP yanıtı iki bölümden oluşur ve sıralaması pazarlığa kapalıdır: önce durum satırı ve başlıklar, ardından boş bir satır, sonra gövde yani sayfanın içeriği. Sunucu bu boş satırı yazdığı anda başlık bölümünü kapatmış olur.
header(), setcookie(), session_start(), header_remove() ve durum kodu belirleyen çağrılar başlık bölümüne yazar. echo, print, HTML metni ve hatta tek bir boşluk karakteri ise gövdeye yazar. Gövdeye tek bir bayt gittikten sonra başlık yazmak isterseniz, bu istek fiziksel olarak imkânsızdır: o baytlar çoktan ağa çıkmıştır. PHP de bunu size bir uyarı ile bildirir ve header() çağrısını sessizce yok sayar.
Bunu iki satırla kendiniz üretebilirsiniz:
<?php
echo "Merhaba"; // gövdeye yazıldı, başlık bölümü kapandı
header('Location: /panel.php'); // Warning: Cannot modify header information
Sonuç şu olur: kullanıcı yönlendirilmez, sayfada "Merhaba" yazısı ve altında uyarı kalır. Kod mantığınız doğrudur, sıralama yanlıştır. Bu yüzden hatanın çözümü header() çağrısını değiştirmek değil, ondan önce sızan çıktıyı bulup kesmektir.
Aynı mekanizma beyaz ekran hatasının tam tersidir: orada hiç çıktı yoktur, burada fazladan çıktı vardır. İkisi de aynı yerden, PHP'nin çıktı davranışından beslenir.
Sebep 1: PHP Açılış Etiketinden Önceki Boşluk#
En yaygın nedendir ve gözle görülmez. Dosyanın ilk karakteri < olmalıdır. Onun önünde bir boşluk, bir sekme ya da bir satır sonu varsa, PHP o karakterleri "PHP dışı içerik" sayar ve olduğu gibi tarayıcıya basar. Ekranda hiçbir şey görünmez, çünkü tek bir boşluk HTML'de fark edilmez; ama başlık bölümü kapanmıştır.
Dosyanın ilk baytlarını doğrudan inceleyin:
# İlk 16 baytı bayt bayt göster
head -c 16 wp-config.php | od -c
# Sağlıklı çıktı şöyle başlamalı:
# 0000000 < ? p h p \n
Çıktının başında \n, boşluk ya da \t görüyorsanız suçluyu buldunuz. Düzeltmek için dosyayı açıp ilk satırdaki her şeyi silin ve dosyanın <?php ile başladığından emin olun. Uzaktan düzenleme yapıyorsanız nano veya vim ile metin düzenleme yazısındaki temel hareketler yeterlidir.
Bu boşluk çoğu zaman insan eliyle girmez. Bir dosyayı Windows'ta Not Defteri ile açıp kaydetmek, FTP istemcisinin metin modunda aktarması ya da kopyala-yapıştır sırasında satır başına düşen görünmez bir karakter, hepsi aynı sonucu verir.
Sebep 2: UTF-8 BOM ile Kaydedilmiş Dosyalar#
Bu, birinci nedenin daha sinsi biçimidir ve editörde hiçbir şekilde göremezsiniz. BOM (Byte Order Mark), bazı editörlerin UTF-8 dosyaların başına eklediği üç baytlık bir imzadır: EF BB BF. Editörünüz bu üç baytı gizler, dosya "boşluksuz" görünür, ama PHP onları sıradan içerik sayar ve tarayıcıya gönderir.
BOM'un tipik kaynakları Windows Not Defteri, bazı eski IDE'lerin varsayılan kaydetme ayarı ve dosyayı bir kez açıp kaydeden Office türü programlardır.
Tespit için iki güvenilir yol var:
# 1) file komutu BOM'u açıkça söyler
file wp-config.php
# wp-config.php: UTF-8 Unicode (with BOM) text
# 2) Tüm projede BOM'lu PHP dosyalarını listele
grep -rl $'\xEF\xBB\xBF' --include='*.php' /home/kullanici/public_html/
Temizlik tek satırlık bir işlemdir. Önce yedek alın, sonra çalıştırın:
# Tek dosyadan BOM'u kaldır (GNU sed)
sed -i '1s/^\xEF\xBB\xBF//' wp-config.php
# Tüm proje genelinde toplu temizlik
find /home/kullanici/public_html -name '*.php' \
-exec sed -i '1s/^\xEF\xBB\xBF//' {} \;
# Doğrula: hiçbir dosya listelenmemeli
grep -rl $'\xEF\xBB\xBF' --include='*.php' /home/kullanici/public_html/
Kalıcı çözüm editör tarafındadır. VS Code'da alt bardaki kodlama göstergesine tıklayıp UTF-8 (BOM'suz) olarak kaydedin; Notepad++ içinde Biçim menüsünden "UTF-8 (BOM'suz)" seçin. Bu ayarı bir kez yaptığınızda aynı hatayı bir daha üretmezsiniz.
Sebep 3: Kapanış Etiketinden Sonraki Boş Satır#
Dosyanın sonundaki ?> etiketinden sonra kalan boşluk ya da boş satır, tam olarak aynı sonucu doğurur. PHP, kapanış etiketinin hemen ardından gelen tek bir satır sonunu kendisi yutar; ikinci satır sonundan itibaren her şey çıktıya gider.
Dosyanın son baytlarına bakın:
# Son 16 baytı incele
tail -c 16 functions.php | od -c
# Sonunda fazladan boşluk kalan dosyaları toplu listele
find . -name '*.php' -print0 | \
xargs -0 perl -0777 -ne 'print "$ARGV\n" if /\?>[\s]{2,}\z/'
Çözüm, boşluğu silmek değil, kapanış etiketini tamamen kaldırmaktır. İçinde yalnızca PHP kodu bulunan bir dosyanın sonuna ?> yazmak zorunlu değildir; PHP dosya sonunda zaten kod modundan çıkar. Modern kodlama standartları bu yüzden saf PHP dosyalarında kapanış etiketini yasaklar. Etiket yoksa, arkasında kalan boşluk da hiçbir zaman sorun çıkaramaz.
Yani ideal dosya sonu şöyledir:
function siteyi_baslat() {
// ...
}
add_action('init', 'siteyi_baslat');
Kapanış etiketi yok, dolayısıyla arkasında ne olduğunun önemi de yok.
Sebep 4: Erken Çıktı, var_dump ve PHP Uyarısının Kendisi#
Görünmez karakterleri elediyseniz geriye gerçek çıktı kalır. Burada üç ayrı senaryo vardır ve üçüncüsü en çok kafa karıştıranıdır.
Unutulmuş hata ayıklama satırları. Bir sorunu çözerken eklenen var_dump($kullanici); ya da print_r($_POST); satırı yerinde kalmıştır. Genellikle sayfanın en üstünde bir dizi çıktısı olarak görünür, dolayısıyla fark edilmesi kolaydır.
HTML ile PHP'nin karışması. Şablon dosyanızda header() çağrısından önce herhangi bir HTML satırı, hatta bir yorum satırı bile varsa çıktı başlamıştır. Yönlendirme ve çerez işlemleri her zaman dosyanın en tepesinde, hiçbir HTML üretilmeden yapılmalıdır.
PHP uyarısının kendisi çıktıdır. Bu, en çok zaman kaybettiren senaryodur. Diyelim ki koda bir Undefined array key uyarısı düşüyor ve display_errors açık. PHP bu uyarıyı ekrana basar; basılan metin gövdeye yazılır; birkaç satır sonraki header() çağrısı da "headers already sent" hatası verir. Yani ekranda iki hata görürsünüz ama gerçekte tek bir sorun vardır ve o da ilk uyarıdır. İkincisi onun gölgesidir.
Bu yüzden canlı sitelerde display_errors kapalı, log_errors açık olmalıdır. Ayrıntılı yapılandırma için PHP hata raporlamayı yönetme yazısına bakın; özeti şudur:
; Canlı sunucu için doğru ayar
display_errors = Off
log_errors = On
error_log = /home/kullanici/logs/php-error.log
error_reporting = E_ALL & ~E_DEPRECATED
Uyarılar ekrana değil dosyaya gittiğinde, hem headers already sent hatası ortadan kalkar hem de gerçek sorunu hata kayıtlarından sakin sakin okuyabilirsiniz.
Çıktının Nerede Başladığını Kodla Bulmak#
Mesajdaki dosya yolu bir eklenti tarafından üretilmişse ya da mesaj hiç görünmüyorsa, PHP size doğrudan sorabileceğiniz bir fonksiyon sunar. headers_sent() iki değişkeni referansla doldurur:
<?php
// Şüphelendiğiniz noktaya geçici olarak yerleştirin
if (headers_sent($dosya, $satir)) {
error_log("Çıktı burada başladı: {$dosya} satır {$satir}");
} else {
error_log('Bu noktada henüz çıktı gönderilmedi.');
}
Bu bloğu kodun farklı noktalarına taşıyarak çıktının hangi include arasında başladığını daraltabilirsiniz. Sonucu error_log() ile yazdırmak önemlidir; echo ile yazdırırsanız teşhis aracınızın kendisi yeni bir çıktı kaynağı olur.
Bir de sık yapılan ve bu hatayla karıştırılan bir yanlış var: yönlendirmeden sonra betiği durdurmamak. header('Location: ...') yalnızca bir başlık gönderir, betiği bitirmez. Altındaki kod çalışmaya devam eder ve korumak istediğiniz sayfanın içeriği tarayıcıya sızabilir.
header('Location: https://ornek.com/panel', true, 302);
exit; // exit yoksa aşağıdaki kod çalışmaya devam eder
ob_start() ve output_buffering Neden Sadece Pansuman?#
Çıktı tamponlama, PHP'nin ürettiği çıktıyı hemen göndermek yerine bellekte biriktirmesidir. Tampon açıkken başlıklar hâlâ değiştirilebilir, çünkü henüz hiçbir şey ağa çıkmamıştır. Bu yüzden ob_start() eklemek hatayı gerçekten susturur.
Sorun şurada: tamponlama sızıntıyı durdurmaz, sadece geciktirir. Dosyanızın başındaki BOM hâlâ oradadır ve tampon boşaldığında sayfanın en başına, <!DOCTYPE html> satırından bile önce yazılır. Sonuçları şunlardır:
- Tarayıcı sayfayı standartlara uygun modda değil, "quirks" modunda açabilir ve yerleşim bozulur.
- JSON döndüren bir uç noktanın çıktısı, başındaki görünmez baytlar yüzünden geçersiz JSON hâline gelir ve istemci tarafında ayrıştırma hatası verir.
- Dosya indirme işlemlerinde indirilen dosyanın ilk baytları bozulur.
Yani tamponlama, gizli bir hatayı daha zor bulunan bir hataya dönüştürür. Doğru kullanımı, hatayı örtmek değil bilinçli bir mimari tercih olmasıdır: bir uygulamanın tüm yanıtı toplayıp tek seferde göndermesi meşrudur.
Yine de sunucu genelinde açmanız gereken bir durum olursa yer şudur:
; php.ini ya da PHP-FPM/CGI kurulumlarında site kökündeki .user.ini
output_buffering = 4096
.user.ini kullanıyorsanız değişikliğin devreye girmesi varsayılan olarak beş dakikaya kadar gecikebilir; hemen test edip "çalışmadı" diye vazgeçmeyin. Kalıcı düzeltmeyi yaptıktan sonra bu ayarı geri almanız gerekmez, ama ona güvenerek dosya temizliğini atlamayın.
WordPress'te En Sık Kaynak: functions.php ve Eklentiler#
WordPress kurulumlarında bu hata neredeyse her zaman üç dosyadan birinden gelir: wp-config.php, aktif temanın functions.php dosyası ya da elle düzenlenmiş bir eklenti dosyası. Sebep de hep aynıdır: dosya panel üzerinden ya da Not Defteri ile düzenlenmiş, sonuna boş satır eklenmiş veya BOM'lu kaydedilmiştir.
Belirti, sebep ve çözümü hızlıca eşleştirin:
| Belirti | Muhtemel sebep | Çözüm |
|---|---|---|
Hata mesajında wp-config.php:1 yazıyor | Dosya başında BOM veya boşluk | file ile doğrulayın, BOM'u silin |
| Bir eklentiyi güncelledikten sonra başladı | Eklenti dosyasının sonunda boşluk | Eklentiyi yeniden yükleyin, elle düzenlemeyi geri alın |
| Temayı düzenledikten sonra başladı | functions.php sonunda boş satır | Kapanış etiketini tamamen kaldırın |
| Panele giriş yapılamıyor, yönlendirme dönmüyor | Çerez ve oturum başlıkları yazılamıyor | Aynı iki nedeni sırayla eleyin |
| Sadece belirli bir sayfada çıkıyor | O sayfanın şablonunda erken çıktı | Şablonda header() öncesi HTML arayın |
Panele hiç giremiyorsanız FTP ya da dosya yöneticisi üzerinden şu sırayı izleyin: önce wp-config.php dosyasının ilk ve son baytlarını kontrol edin, sonra aktif temanın functions.php dosyasını aynı şekilde inceleyin, ardından wp-content/plugins altındaki en son değiştirilmiş dosyayı bulun:
# Son 24 saatte değişmiş PHP dosyalarını listele
find wp-content -name '*.php' -mtime -1 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort
# Bu dosyaların hepsinde BOM taraması yap
grep -rl $'\xEF\xBB\xBF' --include='*.php' wp-content/
Tema dosyalarını doğrudan düzenlemek zorunda kalmamak için alt tema kullanmak bu tür kazaların büyük bölümünü baştan engeller: ana tema güncellendiğinde düzenlemeleriniz kaybolmaz ve panelden yapılan riskli düzenlemelere gerek kalmaz.
Adım Adım Teşhis Akışı#
Hatayı gördüğünüz andan itibaren izleyeceğiniz sıra şudur. Her adım bir sonrakine geçmeden önce sonuç vermelidir:
- Mesajı okuyun. Parantez içindeki
output started atkısmındaki dosya yolunu ve satır numarasını not alın. Diğer dosya yolunu tamamen görmezden gelin. - Satır numarasına göre yön belirleyin. Satır 1 ise başa, dosyanın sonuna yakınsa sona, ortalarda ise o satırın kendisine bakın.
- İlk baytları inceleyin.
head -c 16 dosya.php | od -cçıktısı<karakteriyle başlamıyorsa boşluk ya da BOM vardır. - BOM taraması yapın.
grep -rl $'\xEF\xBB\xBF' --include='*.php' .komutu boş dönmelidir. - Kapanış etiketlerini temizleyin. Saf PHP dosyalarında
?>etiketini kaldırın. - Uyarıları ekrandan çekin.
display_errors = Off,log_errors = Onyapın ve gerçek uyarıyı günlükten okuyun. - Hâlâ sürüyorsa daraltın.
headers_sent($dosya, $satir)bloğunu farklı noktalara koyarak sızıntının başladığı include'u bulun.
Bu akış hatanın nedenini neredeyse her zaman ilk dört adımda ortaya çıkarır. Hata düzeldikten sonra ekranda başka bir mesaj belirirse paniklemeyin: gerçek sorun zaten oradaydı, headers already sent onu gizliyordu. Eğer sayfa bu kez hiç açılmıyorsa ve sunucu genel bir hata döndürüyorsa, 500 Internal Server Error yazısındaki log okuma adımlarıyla devam edin.
Sıkça Sorulan Sorular#
Hata mesajındaki hangi dosya yoluna bakmalıyım?#
Parantez içindeki output started at kısmındaki dosya ve satır numarasına bakın. Mesajın sonundaki in ... on line ... kısmı yalnızca header() çağrısının yapıldığı yerdir ve genellikle çekirdek bir dosyadır; orada hiçbir hata yoktur. Suçlu, çıktıyı ilk gönderen dosyadır. Bu ayrımı kavramak, hata ayıklama süresini dakikalardan saniyelere indirir.
ob_start() eklersem sorun çözülür mü?#
Hata mesajı kaybolur ama sebep yerinde kalır. Çıktı tamponlaması sızan baytları yok etmez, sadece geciktirir; tampon boşaldığında o baytlar sayfanın en başına yazılır. Bu da HTML'in quirks modunda açılmasına, JSON yanıtlarının geçersiz hâle gelmesine ve indirilen dosyaların bozulmasına yol açabilir. Önce dosyayı temizleyin, tamponlamayı bilinçli bir tercih olarak kullanın.
Dosyada gerçekten boşluk göremiyorum, yine de hata alıyorum. Neden?#
Büyük olasılıkla dosya UTF-8 BOM ile kaydedilmiştir. BOM üç baytlık görünmez bir imzadır ve editörler onu göstermez. file dosya.php komutu çıktısında "with BOM" ifadesi geçiyorsa neden budur. Editörünüzün kodlama ayarını "UTF-8 BOM'suz" olarak değiştirin, aksi hâlde dosyayı her kaydettiğinizde imza geri gelir.
Neden PHP dosyalarının sonuna kapanış etiketi koymamalıyım?#
Yalnızca PHP kodu içeren bir dosyada kapanış etiketi hiçbir işe yaramaz; PHP dosya sonunda kod modundan zaten çıkar. Buna karşılık etiketten sonra kalan tek bir boş satır bile çıktıya dönüşür ve başlık göndermeyi imkânsız kılar. Etiketi kaldırdığınızda arkasında ne kaldığının önemi kalmaz. Modern kodlama standartları bu yüzden onu yasaklar.
Aynı anda iki farklı hata görüyorum, hangisini çözmeliyim?#
Önce alttaki değil, üstteki hatayı çözün. display_errors açıkken PHP'nin ekrana bastığı bir uyarı, kendisi de bir çıktıdır ve arkasından gelen her header() çağrısını "headers already sent" hatasına düşürür. Yani ilk uyarıyı giderdiğinizde ikincisi kendiliğinden kaybolur. Canlı sitelerde uyarıların ekrana değil günlüğe yazılması bu zinciri baştan engeller.
Yönlendirme satırından sonra exit yazmam gerekir mi?#
Evet. header('Location: ...') yalnızca bir başlık gönderir, betiği durdurmaz. Altındaki kod çalışmaya devam eder; gizlemek istediğiniz sayfanın içeriği üretilir ve bazı durumlarda tarayıcıya sızar. Yönlendirmeyi bir güvenlik kontrolü olarak kullanıyorsanız exit satırını eklemek isteğe bağlı değil, zorunludur.
Bu hata sunucumun yetersiz olduğunu mu gösterir?#
Hayır. Bellek limiti veya çalışma süresi aşımı gibi hatalardan tamamen farklıdır; kaynakla hiçbir ilgisi yoktur. Aynı hata en güçlü sunucuda da, en küçük paylaşımlı hesapta da birebir aynı şekilde ortaya çıkar. Nedeni bir kod ve dosya biçimi sorunudur, dolayısıyla çözümü de paket yükseltmek değil, çıktının nereden sızdığını bulmaktır.