Как грамотно отвечать на несуществующие страницы: обработка 404 в PHP
Методы обработки ошибки 404 в PHP
Как реализовать корректный HTTP-ответ 404 и показать пользователю понятную страницу?
Основной способ - явно установить HTTP-статус 404 и вывести содержимое страницы ошибки. Это гарантирует, что клиент (браузер, поисковый робот) получит правильный код ответа.
<?php
// main.php - точка входа
$page = $_GET['page'] ?? 'home';
$allowedPages = ['home', 'about', 'contact'];
if (!in_array($page, $allowedPages)) {
http_response_code(404);
// или header('HTTP/1.0 404 Not Found');
echo '<h1>Страница не найдена</h1>';
exit;
}
echo '<h1>Добро пожаловать на страницу ' . htmlspecialchars($page) . '</h1>';
?>
404 ошибки php (обработка ошибки 404 в php)
Пояснение:
- http_response_code(404) - устанавливает код ответа (PHP >= 5.4).
- После установки кода следует прекратить выполнение сценария (exit), чтобы не выводить лишнюю информацию.
Типичная ошибка:
Если до вызова http_response_code() уже был выведен какой-то текст (например, пробел или BOM), заголовки не будут отправлены. Решение - проверить отсутствие вывода перед отправкой заголовков или использовать буферизацию (ob_start()).
Цель: простой и предсказуемый способ для небольших проектов без фреймворков.
Варианты реализации
Как перенаправить все 404 ошибки на единый обработчик через .htaccess?
Метод подходит для серверов Apache. Создаётся файл .htaccess в корне сайта:
ErrorDocument 404 /404.php
В файле 404.php:
<?php
http_response_code(404);
echo '<h1>Страница не найдена (из .htaccess)</h1>';
?>
Проблема:
Если файл 404.php сам не существует, сервер покажет пустую страницу или другую ошибку. Решение - всегда проверять существование файла и иметь запасной вариант в конфигурации сервера.
Цель: централизованная обработка 404 для статических файлов и отсутствующих маршрутов без изменения PHP-кода.
Как обработать 404 с помощью исключений и кастомного обработчика?
Создаётся класс исключения NotFoundException, который выбрасывается в коде при отсутствии ресурса. В глобальном обработчике перехватывается и устанавливается 404.
<?php
class NotFoundException extends Exception {}
function exception_handler($exception) {
if ($exception instanceof NotFoundException) {
http_response_code(404);
echo '<h1>404 - ' . $exception->getMessage() . '</h1>';
} else {
http_response_code(500);
echo '<h1>Внутренняя ошибка сервера</h1>';
}
}
set_exception_handler('exception_handler');
// Пример использования
$id = $_GET['id'] ?? null;
if (!$id) {
throw new NotFoundException('Не указан ID');
}
echo 'ID: ' . htmlspecialchars($id);
?>
Типичная ошибка:
Если исключение выброшено внутри блока try/catch, глобальный обработчик не сработает. Решение - не перехватывать NotFoundException локально или пробрасывать его дальше.
Цель: интеграция обработки ошибок с механизмом исключений, удобно для ООП-приложений.
Как использовать пользовательскую функцию ошибок для генерации 404?
PHP-функция set_error_handler() не обрабатывает ошибки уровня E_USER_ERROR, E_USER_WARNING и E_USER_NOTICE. Можно эмулировать 404 с помощью триггера:
<?php
function customError($errno, $errstr) {
if ($errno == E_USER_ERROR && strpos($errstr, '404') !== false) {
http_response_code(404);
echo '<h1>Пользовательская 404 ошибка</h1>';
exit;
}
}
set_error_handler('customError');
trigger_error('404: Страница не найдена', E_USER_ERROR);
?>
Проблема:
Такой подход не даёт правильный HTTP-статус до вызова обработчика, если уже был вывод. Решение - использовать буферизацию вывода (ob_start()) и очищать буфер перед отправкой заголовков.
Цель: для проектов, где уже используется свой обработчик ошибок, можно добавить логику 404.
Как в современных фреймворках (Laravel, Symfony) настраивается обработка 404?
В Laravel достаточно создать представление resources/views/errors/404.blade.php. В Symfony файл шаблона templates/bundles/TwigBundle/Exception/error404.html.twig. Код ответа устанавливается автоматически.
<!-- Laravel resources/views/errors/404.blade.php -->
@extends('layouts.app')
@section('content')
<h1>404 - Страница не найдена</h1>
<p>Запрашиваемая страница отсутствует.</p>
@endsection
Если требуется кастомная логика (например, логирование), в Laravel используется App\Exceptions\Handler:
public function render($request, Throwable $exception)
{
if ($exception instanceof NotFoundHttpException) {
Log::warning('404 на URL: ' . $request->fullUrl());
}
return parent::render($request, $exception);
}
Цель: использование готового механизма фреймворка для единообразной обработки ошибок.
Расширенные примеры обработки 404
Пример 1: Проверка существования файла и возврат 404
Сценарий: пользователь запрашивает файл изображения. Если файла нет, отдаётся 404 и подменное изображение.
<?php
$file = $_GET['file'] ?? null;
if (!$file || !file_exists(__DIR__ . '/images/' . basename($file))) {
http_response_code(404);
// отдаём заглушку
header('Content-Type: image/png');
readfile(__DIR__ . '/images/notfound.png');
exit;
}
header('Content-Type: image/png');
readfile(__DIR__ . '/images/' . basename($file));
?>
Результат: при отсутствии файла браузер получает HTTP 404 и изображение-заглушку. Важно: заголовок 404 отправляется, но тело ответа содержит нестандартный контент - допустимо.
Пример 2: Обработка 404 в REST API с JSON-ответом
Для API вместо HTML возвращается JSON с кодом ошибки.
<?php
function json404($message = 'Not Found') {
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => $message, 'code' => 404]);
exit;
}
// Маршрутизация
$route = $_SERVER['REQUEST_URI'];
if (preg_match('#^/api/user/(\d+)$#', $route, $matches)) {
$userId = (int)$matches[1];
$user = getUserById($userId); // может вернуть false
if (!$user) {
json404('User not found');
}
echo json_encode($user);
} else {
json404('Invalid API endpoint');
}
?>
Результат: клиент получает HTTP 404 и JSON:
{"error":"User not found","code":404}
Пример 3: Кастомный обработчик 404 с логированием и разными шаблонами
Комбинируем .htaccess с PHP-скриптом, который логирует запрос и показывает разные страницы в зависимости от user-agent.
<?php
// 404.php
http_response_code(404);
// Логирование
$log = sprintf("[%s] 404 %s от %s\n", date('Y-m-d H:i:s'), $_SERVER['REQUEST_URI'], $_SERVER['REMOTE_ADDR']);
file_put_contents(__DIR__ . '/404.log', $log, FILE_APPEND);
// Выбор шаблона
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (preg_match('/bot|spider|crawler/i', $ua)) {
// для поисковых ботов - минимум кода
echo '<!DOCTYPE html><title>404</title><h1>Not found</h1>';
} else {
// полная страница с предложением перейти на главную
include __DIR__ . '/templates/404_full.php';
}
?>
Результат: для ботов - простая страница, для людей - кастомный шаблон. Записи в логе позволяют анализировать частоту ошибок.
Пример 4: Обработка 404 через middleware в самописном маршрутизаторе
Микрофреймворк: маршрутизатор вызывает обработчик, если ни один маршрут не подошёл.
<?php
class Router {
private $routes = [];
public function add($pattern, $callback) {
$this->routes[$pattern] = $callback;
}
public function dispatch($uri) {
foreach ($this->routes as $pattern => $callback) {
if (preg_match($pattern, $uri, $params)) {
array_shift($params);
return call_user_func_array($callback, $params);
}
}
// Ни один маршрут не совпал
$this->handle404($uri);
}
private function handle404($uri) {
http_response_code(404);
header('Content-Type: text/html; charset=utf-8');
echo '<h1>404 - Маршрут не найден: ' . htmlspecialchars($uri) . '</h1>';
error_log('404: ' . $uri);
exit;
}
}
$router = new Router();
$router->add('#^/$#', function() { echo 'Главная'; });
$router->add('#^/post/(\d+)$#', function($id) { echo 'Пост ' . $id; });
$router->dispatch($_SERVER['REQUEST_URI']);
?>
Результат: при запросе /test выводится «404 - Маршрут не найден: /test».
Пример 5: Использование буферизации вывода для корректной отправки заголовков 404
Когда в скрипте уже были echo, но нужно переопределить код ответа.
<?php
ob_start();
echo 'Немного контента...';
// Позже выяснили, что страница не найдена
if ($conditionError) {
ob_clean(); // очищаем буфер
http_response_code(404);
echo '<h1>Страница не найдена (после буферизации)</h1>';
}
ob_end_flush();
?>
Результат: даже если ранее был вывод, заголовки 404 устанавливаются корректно. Важно не забыть ob_clean() перед отправкой новых данных.