Отправка HTTP ответов в PHP: от простого к сложному
Основные принципы формирования HTTP ответа
Наиболее эффективный способ отправки HTTP ответа в PHP основан на комбинации функций http_response_code(), header() и прямого вывода тела через echo. Этот подход не требует дополнительных библиотек и даёт полный контроль над заголовками и содержимым.
Пример: функция для возврата JSON ответа с произвольным статусом.
function sendJsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
// Использование:
sendJsonResponse(['message' => 'Успешно', 'id' => 42], 201);
Php response request (обработка ответа http запроса в php)
Пояснение шагов:
http_response_code($statusCode)устанавливает HTTP статус ответа (201 Created).header('Content-Type: application/json; charset=utf-8')сообщает клиенту, что тело содержит JSON.json_encode()преобразует массив в JSON. ФлагJSON_UNESCAPED_UNICODEсохраняет кириллицу,JSON_THROW_ON_ERRORвызывает исключение при ошибке.exitнемедленно завершает выполнение скрипта, чтобы избежать случайного вывода другого содержимого.
Типичные проблемы и их решения:
- Синдром "Headers already sent" возникает, если до вызова header() уже был выведен любой текст (включая пробелы до
<?php). Решение: использовать буферизацию вывода (ob_start()) в начале скрипта или переносить логику заголовков до любого вывода. - Некорректный Content-Type – если не установить заголовок, браузер может интерпретировать ответ как HTML, что вызовет ошибки. Всегда явно указывать MIME-тип.
- Ошибки json_encode – например, при наличии ресурсов или циклических ссылок. Обрабатывать исключение с
JSON_THROW_ON_ERRORи возвращать 500.
Как отправить HTML страницу с динамическим содержимым?
Устанавливается заголовок Content-Type: text/html; charset=utf-8, после чего выводится сгенерированный HTML.
function renderHtml($template, $vars) {
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
extract($vars);
include $template;
exit;
}
renderHtml('views/user.tpl.php', ['name' => 'Анна', 'age' => 28]);
Ошибка: вывод неэкранированных переменных в шаблоне ведёт к XSS-уязвимости. Все переменные необходимо обрабатывать функцией htmlspecialchars().
Как выполнить перенаправление на другой URL?
Используется заголовок Location с кодом 301 (постоянный) или 302 (временный).
header('Location: https://example.com/new-page', true, 301);
exit;
Если после header('Location') не вызвать exit, скрипт продолжит выполнение и может вывести дополнительную информацию, что нарушит редирект. Всегда завершать выполнение.
Как отдать файл на скачивание?
Необходимо отправить заголовки, описывающие файл, и прочитать его содержимое.
$file = '/path/to/document.pdf';
$filename = 'document.pdf';
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($file));
header('Cache-Control: private, max-age=0, must-revalidate');
readfile($file);
exit;
При больших файлах может не хватить памяти для readfile(). Следует увеличить memory_limit или читать файл по частям с помощью fread() в цикле. Также полезно снять ограничение времени выполнения set_time_limit(0).
Как вернуть JSON ответ для AJAX?
Ответ с JSON уже показан в основном решении. Дополнительно можно обрабатывать параметр callback для JSONP.
if (isset($_GET['callback'])) {
header('Content-Type: application/javascript; charset=utf-8');
echo $_GET['callback'] . '(' . json_encode($data) . ');';
} else {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
exit;
Необходимо проверять callback на допустимость (только буквы, цифры, точка, подчёркивание), иначе возможно внедрение кода. Также стоит использовать preg_match('/^[$A-Z_][0-9A-Z_$]*$/i', $callback).
Как отправить ответ с кодом 404 и кастомной страницей?
Устанавливается код 404, выводится HTML.
http_response_code(404);
header('Content-Type: text/html; charset=utf-8');
echo 'Страница не найдена
Проверьте URL.
';
exit;
Если после установки кода 404 скрипт продолжит выполнение и встретит другой код (например, в фреймворке), итоговый статус может быть перезаписан. Используйте exit.
Как установить несколько кук в одном ответе?
Функция setcookie() вызывается для каждой куки перед любым выводом.
setcookie('session_id', 'abc123', time()+3600, '/', '', true, true);
setcookie('theme', 'dark', time()+86400*30, '/', '', true, true);
// далее вывод тела
Куки устанавливаются в заголовки, поэтому вывод до их установки приведёт к ошибке. Также важно указывать параметр httponly и secure для защиты.
Как отправить ответ с поддержкой кэширования (ETag)?
Вычисляется ETag, сравнивается с заголовком If-None-Match; если совпадает – отправляется 304 Not Modified.
$data = getExpensiveData();
$etag = md5(serialize($data));
header('ETag: "' . $etag . '"');
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === '"' . $etag . '"') {
http_response_code(304);
exit;
}
// отдаём данные
echo $data;
Нужно аккуратно обрабатывать кавычки в ETag. Лучше использовать строгое сравнение. Также не забывать про кэширующие заголовки Cache-Control.
Как обработать OPTIONS запрос и вернуть CORS заголовки?
При методе OPTIONS отправляются только заголовки CORS, тело пустое.
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
http_response_code(204);
exit;
}
Необходимо корректно обрабатывать предзапросы (preflight) – они не должны выполнять основную логику. Также нельзя разрешать все методы без разбора.
Расширенные примеры обработки HTTP ответов
Пример 1: JSON ответ с поддержкой JSONP и CORS
Сценарий: API эндпоинт, который может вызываться с другого домена. Если передан параметр callback, ответ оборачивается в функцию для JSONP. Добавляются CORS заголовки.
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
$data = ['name' => 'Иван', 'age' => 30];
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
if (isset($_GET['callback'])) {
$callback = $_GET['callback'];
// проверка безопасности
if (!preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $callback)) {
http_response_code(400);
echo 'Invalid callback';
exit;
}
header('Content-Type: application/javascript; charset=utf-8');
echo $callback . '(' . $json . ');';
} else {
header('Content-Type: application/json; charset=utf-8');
echo $json;
}
exit;
Результат: При запросе /api.php?callback=myFunc заголовки будут:
HTTP/2 200
Access-Control-Allow-Origin: *
Content-Type: application/javascript; charset=utf-8
myFunc({"name":"Иван","age":30});
Пример 2: Потоковая передача большого файла с управлением памятью
Когда файл слишком велик для полного чтения в память, используется чтение по частям с fread() и сбросом буфера.
$file = '/var/logs/bigfile.log';
$filename = 'bigfile.log';
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($file));
$handle = fopen($file, 'rb');
if ($handle) {
while (!feof($handle)) {
$chunk = fread($handle, 8192);
echo $chunk;
ob_flush();
flush();
}
fclose($handle);
}
exit;
Результат: Файл будет отправлен клиенту частями, что снижает потребление памяти на сервере. Важно убедиться, что output_buffering отключён или настроен правильно.
Пример 3: Ответ с использованием PSR-7 Response и эмиттера
Современный подход через PSR-7 интерфейсы. Для примера используется библиотека nyholm/psr7 и эмиттер laminas/laminas-httphandlerrunner.
require 'vendor/autoload.php';
use Nyholm\Psr7\Response;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
$data = ['status' => 'ok', 'timestamp' => time()];
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
$response = new Response(
200,
['Content-Type' => 'application/json; charset=utf-8'],
$body
);
$emitter = new SapiEmitter();
$emitter->emit($response);
Результат: Эмиттер отправляет заголовки и тело, используя стандартный SAPI. Этот код не зависит от глобальных функций и упрощает тестирование.
Пример 4: Ответ с ошибкой 500 и логированием
Если в скрипте возникает непредвиденная ситуация, следует вернуть правильный код ошибки и залогировать детали.
try {
// опасная операция
$result = someRiskyFunction();
sendJsonResponse($result);
} catch (\Throwable $e) {
error_log($e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'Внутренняя ошибка сервера']);
exit;
}
Результат: Клиент получает HTTP 500 с JSON ошибкой, а разработчик видит детали в логах. Не следует раскрывать пользователю внутренние сообщения.
Пример 5: Отправка изображения из базы данных (BLOB)
Если файл хранится в бинарном поле, можно отдать его напрямую без промежуточного файла.
$imageData = $db->query('SELECT data, mime_type FROM images WHERE id = ?', [$id])->fetchColumn();
if (!$imageData) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $imageData['mime_type']);
header('Content-Length: ' . strlen($imageData['data']));
header('Cache-Control: public, max-age=86400');
echo $imageData['data'];
exit;
Результат: Браузер отображает изображение, если mime_type корректен (image/png, image/jpeg). Проблема: большие BLOB могут нагружать память; для больших файлов лучше использовать потоковые запросы к БД.
Пример 6: Ответ с поддержкой сжатия gzip
Если клиент поддерживает gzip, можно сжать вывод для уменьшения трафика. В PHP есть встроенная обработка ob_gzhandler.
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '', 'gzip')) {
ob_start('ob_gzhandler');
} else {
ob_start();
}
echo 'Содержимое ответа, которое будет автоматически сжато.';
$output = ob_get_clean();
header('Content-Length: ' . strlen($output));
echo $output;
exit;
Результат: Заголовок Content-Encoding: gzip будет добавлен автоматически, если был вызван ob_gzhandler. Важно не применять двойное сжатие (например, если веб-сервер уже сжимает).