Cabeceras HTTP
Cuando un servidor responde a una petición HTTP, antes del cuerpo envía un bloque de cabeceras: metadatos que le dicen al navegador qué tipo de contenido viene, si debe redirigir a otra URL, si puede cachear la respuesta, etc. PHP permite enviar y modificar esas cabeceras con la función header().
La regla fundamental
Las cabeceras deben enviarse antes de cualquier salida: antes de echo, antes de espacios en blanco, antes del cierre ?>. Una vez que PHP empieza a enviar el cuerpo de la respuesta, las cabeceras ya fueron. Si llamas a header() tarde, PHP emite un aviso y la cabecera se ignora:
<?php
// Correcto: header() antes de cualquier salida
header("Content-Type: text/plain");
echo "Hola";
<?php
echo "Algo de texto";
header("Location: /otra-pagina.php"); // Error: cabeceras ya enviadas
headers_sent() devuelve true si ya se enviaron cabeceras. Útil para depurar:
<?php
if (!headers_sent()) {
header("X-Mi-Cabecera: valor");
}
Redirecciones
La cabecera Location redirige al navegador a otra URL. Siempre llama a exit o die justo después: sin él, el script PHP sigue ejecutándose aunque el navegador ya no espere más respuesta.
<?php
// Redirección temporal (302 por defecto)
header("Location: /inicio.php");
exit;
// Redirección permanente (301) — los buscadores actualizan el índice
header("Location: /nueva-url.php", true, 301);
exit;
Nota: Usa 301 solo cuando la URL haya cambiado definitivamente. El 302 (por defecto) es para redirecciones temporales, como tras procesar un formulario.
Tipo de contenido
La cabecera Content-Type indica al navegador cómo interpretar la respuesta. Si no la envías, PHP usa text/html; charset=UTF-8 por defecto. Los tipos más habituales:
<?php
// JSON para APIs y respuestas AJAX
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["ok" => true]);
exit;
// Texto plano
header("Content-Type: text/plain; charset=utf-8");
echo "Sin HTML aquí";
// XML
header("Content-Type: application/xml; charset=utf-8");
echo "<?xml version=\"1.0\"?><respuesta/>";
Descarga de ficheros
Para forzar que el navegador descargue un archivo en lugar de mostrarlo, se combinan varias cabeceras:
<?php
$ruta = __DIR__ . "/informes/informe-2026.pdf";
$nombre = "informe-2026.pdf";
$tamano = filesize($ruta);
header("Content-Type: application/pdf");
header("Content-Disposition: attachment; filename=\"" . $nombre . "\"");
header("Content-Length: " . $tamano);
header("Cache-Control: private, no-cache");
readfile($ruta);
exit;
Usa inline en lugar de attachment si quieres que el navegador intente mostrar el archivo (por ejemplo, un PDF en el visor integrado):
header("Content-Disposition: inline; filename=\"informe.pdf\"");
Nota: Valida siempre que el archivo existe y que el usuario tiene permiso para descargarlo antes de servirlo. Nunca construyas la ruta directamente desde un parámetro de usuario sin sanitizarla: es un vector clásico de path traversal (../../../etc/passwd).
Control de caché
Páginas con datos sensibles (panel de usuario, resultados de búsqueda) no deben guardarse en caché del navegador:
<?php
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
Para recursos estáticos que cambian poco, se puede indicar un tiempo de caché:
<?php
// Cacheable durante una hora
header("Cache-Control: public, max-age=3600");
Buffer de salida
Si necesitas enviar cabeceras pero parte del código ya genera salida (por ejemplo, en un include que imprime algo antes de que el controlador decida redirigir), el buffer de salida captura esa salida y retrasa el envío real hasta que llames a ob_end_flush():
<?php
ob_start(); // A partir de aquí, todo echo/print va al buffer
include "cabecera.php"; // podría imprimir algo
header("Location: /login.php"); // sigue funcionando porque aún no se envió nada al cliente
ob_end_clean(); // descarta el buffer
exit;
Nota: ob_start() es útil como parche puntual, pero la solución de fondo es estructurar el código para que todas las decisiones de cabecera se tomen antes de producir salida.
Recapitulación
- Las cabeceras se envían con
header("Nombre: valor")y deben ir antes de cualquier salida. - Redirección:
header("Location: /url")seguido deexit. Usa el código 301 para cambios permanentes. Content-Typeindica al navegador cómo interpretar la respuesta:application/json,text/plain,application/pdf…- Para forzar descarga:
Content-Disposition: attachment; filename="..."másreadfile(). headers_sent()permite comprobar si ya se enviaron;ob_start()retrasa el envío como último recurso.
En la próxima lección: cookies en PHP: cómo crearlas, leerlas, modificarlas y eliminarlas.