Отправка HTTP ответов в PHP: от простого к сложному

Раздел: Веб-программирование PHP -> Работа с HTTP ответами

Основные принципы формирования 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)

Пояснение шагов:

  1. http_response_code($statusCode) устанавливает HTTP статус ответа (201 Created).
  2. header('Content-Type: application/json; charset=utf-8') сообщает клиенту, что тело содержит JSON.
  3. json_encode() преобразует массив в JSON. Флаг JSON_UNESCAPED_UNICODE сохраняет кириллицу, JSON_THROW_ON_ERROR вызывает исключение при ошибке.
  4. 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. Важно не применять двойное сжатие (например, если веб-сервер уже сжимает).

обработка ответа HTTP запроса в PHP - comments

En
Php response request (php)