PHP header(): HTTP‑заголовки, редирект и кеширование — практическое руководство
Функция header() — один из базовых инструментов PHP для управления ответом сервера. С её помощью вы задаёте Content-Type, выполняете редиректы, настраиваете кеширование, CORS и даже раздачу файлов. В статье — практичные паттерны и готовые сниппеты, чтобы быстро решать повседневные задачи и избегать подводных камней.
Когда отправляются заголовки и откуда берётся «Headers already sent»
Заголовки уходят клиенту до первого байта тела ответа. Если вы что-то выводили (echo, var_dump, HTML) — менять заголовки уже нельзя. Проверить можно так:
<?php
if (headers_sent($file, $line)) {
error_log("Заголовки уже отправлены в $file:$line");
}
Частые причины ошибки:
- Пробелы или переносы строк до <?php или после ?>
- BOM в файлах (UTF‑8 without BOM обязателен)
- Ранняя отладочная печать
На время разработки можно включить буферизацию как «подушку безопасности», но лучше дисциплинировать вывод:
<?php
ob_start(); // Стартуем как можно раньше
// ... ваш код
ob_end_flush();
Синтаксис header() и установка статуса
Базовые примеры отправки заголовков и статуса ответа:
<?php
header('Content-Type: application/json; charset=UTF-8');
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
// Редирект со статусом
header('Location: /login', true, 302);
exit; // Завершаем скрипт после редиректа
// Либо отдельно код:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
Аргумент replace (второй) позволяет не перезаписывать уже установленный одноимённый заголовок: header('Cache-Control: max-age=60', false);
Редиректы правильно: 301/302/303/307/308 и PRG-паттерн
- 301 — постоянный редирект (SEO, кэшируется браузером).
- 302 — временный (по умолчанию в PHP).
- 303 — после POST перенаправляет на GET (PRG-паттерн).
- 307/308 — сохраняют метод (307 временный, 308 постоянный).
<?php
// Пример PRG после успешного логина
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ok = login($_POST['email'] ?? '', $_POST['pass'] ?? '');
if ($ok) {
header('Location: /dashboard', true, 303); // POST -> GET
exit;
}
}
// Показываем форму логина (GET)
JSON API: Content-Type, статус и кеш одним хелпером
<?php
function sendJson($data, int $status = 200, int $cacheTtl = 0): void {
header('Content-Type: application/json; charset=UTF-8');
http_response_code($status);
if ($cacheTtl > 0) {
header('Cache-Control: public, max-age=' . $cacheTtl);
} else {
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
}
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
// Использование
sendJson(['ok' => true, 'items' => [1,2,3]], 200, 60);
Кеширование: Cache-Control, ETag и Last-Modified
Для динамики хватит Cache-Control. Для тонкой валидации — ETag и Last-Modified с поддержкой условных запросов.
<?php
$content = render_article($id);
$etag = 'W/"' . md5($content) . '"';
$lastModified = gmdate('D, d M Y H:i:s', filemtime(article_path($id))) . ' GMT';
header('ETag: ' . $etag);
header('Last-Modified: ' . $lastModified);
header('Cache-Control: public, max-age=300');
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
$ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
if ($ifNoneMatch === $etag || $ifModifiedSince === $lastModified) {
http_response_code(304); // Not Modified
exit;
}
echo $content;
Раздача файлов: Content-Disposition и безопасность
<?php
$file = __DIR__ . '/reports/2026.pdf';
if (!is_file($file)) {
http_response_code(404);
exit('File not found');
}
header('Content-Type: application/pdf');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename="report-2026.pdf"');
header('X-Content-Type-Options: nosniff');
readfile($file);
exit;
Никогда не подставляйте имена файлов напрямую из пользовательского ввода без проверки пути и белых списков расширений.
Базовый CORS для простых сценариев
<?php
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = ['https://app.example.com'];
if (in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 600');
http_response_code(204);
exit;
}
Частые ошибки и как их избежать
- BOM и пробелы: сохраняйте файлы в UTF‑8 без BOM, не закрывайте PHP-теги в файлах с чистым PHP-кодом.
- Вывод до заголовков: никакого echo/var_dump до header(). Используйте лог в файл вместо вывода на экран.
- Нет exit после редиректа: всегда завершайте скрипт после Location.
- Неверный статус при POST‑редиректе: используйте 303 для PRG, 307/308 — когда важно сохранить метод.
- Кеш не инвалидируется: обновляйте ETag/Last-Modified и корректно отвечайте 304.
Отладка заголовков
<?php
print_r(headers_list()); // Какие заголовки уже выставлены
if (headers_sent($f, $l)) {
error_log("Headers sent at $f:$l");
}
Мини‑шпаргалка
- JSON:
header('Content-Type: application/json; charset=UTF-8') - Редирект:
header('Location: /url', true, 302); exit; - PRG:
303 See Other - Кеш:
Cache-Control: public, max-age=... - Валидация кеша:
ETag,Last-Modified+304 - Файлы:
Content-Disposition: attachment - Безопасность:
X-Content-Type-Options: nosniff,X-Frame-Options
Что дальше изучать
Если хотите системно прокачать PHP с нуля до уверенной разработки бэкенда и БД, посмотрите программу и примеры из курса: Прокачать PHP на живых проектах: «PHP и MySQL с Нуля до Гуру 3.0».
Теперь вы знаете, как уверенно управлять HTTP‑заголовками в PHP и избегать «Headers already sent». Сохраните шпаргалку и переиспользуйте сниппеты в своих проектах!
-
Создано 18.05.2026 17:01:07
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.