HTTP para administradores de servidores: guía práctica
Aprende HTTP desde el servidor: métodos, códigos de estado, cabeceras clave, registros de nginx y cómo encajan TLS y HTTP/3 en la operación diaria.
¿Qué es HTTP?
HTTP (protocolo de transferencia de hipertexto) es el conjunto de reglas que un cliente y un servidor web usan para solicitar algo y devolverlo. El cliente envía una solicitud: un método como GET, una ruta como /pricing, una versión del protocolo, una lista de cabeceras y, en algunos casos, un cuerpo. El servidor responde con un código de estado como 200, seguido de sus propias cabeceras y, normalmente, un cuerpo. Cada visita a una página y cada llamada a una API (interfaz de programación de aplicaciones) en el servidor es ese intercambio, repetido.
HTTP no mantiene estado por sí mismo. El servidor no recuerda lo que solicitó hace un segundo, por lo que cualquier comportamiento que actúe como memoria, como una sesión de inicio de sesión, se transporta en una cabecera en cada solicitud. Esta propiedad explica gran parte de lo que sigue: la caché depende completamente de las cabeceras y un equilibrador de carga puede enviar la siguiente solicitud a otro backend sin que nada deje de funcionar.
Todo lo que se explica a continuación muestra cómo funciona este modelo desde el lado del servidor, en el registro de acceso y en la configuración de nginx.
Una solicitud y una respuesta sin procesar, con anotaciones
Esta es una solicitud HTTP/1.1 completa. Una línea en blanco termina las cabeceras. Todo lo que aparece después de esa línea es el cuerpo. Un GET normalmente no tiene cuerpo.
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGETes el método. Indica la operación que se solicita.GETlee,POSTenvía datos,PUTreemplaza,DELETEelimina yHEADsolicita las cabeceras de unGETsin el cuerpo./pricinges la ruta. El nombre de host no forma parte de la línea de solicitud. Por eso existe la siguiente cabecera.HTTP/1.1es la versión del protocolo que utiliza el cliente.Host: example.comindica el sitio que solicita el cliente. HTTP/1.1 lo exige. Por eso nginx responde con400 Bad Requestcuando falta.- El resto son preferencias.
Accept-Encoding: gzipindica que el cliente puede descomprimir. Por tanto, el servidor puede comprimir el cuerpo.
La respuesta tiene la misma estructura, con una línea de estado al principio.
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OKes el código de estado y su frase descriptiva. Lo importante es el código. La frase es decorativa y los clientes la ignoran.Content-Typeindica al cliente cómo debe tratar los bytes siguientes.Content-Lengthes el tamaño del cuerpo en bytes. Así, el cliente sabe dónde termina. Si el tamaño no se conoce de antemano, el servidor envíaTransfer-Encoding: chunkedy marca el final con un fragmento de longitud cero.Cache-Controlindica al navegador y a cualquier caché intermedia durante cuánto tiempo pueden considerar reciente esta respuesta. Cuando su antigüedad superamax-agesegundos, pasa a estar obsoleta. Esto no significa que se elimine: una caché puede conservar una copia obsoleta y volver a validarla con el servidor, en lugar de descargar de nuevo todo el cuerpo.- La línea en blanco posterior a las cabeceras las separa del cuerpo en ambas direcciones.
Los nombres de las cabeceras no distinguen mayúsculas de minúsculas. Además, cada línea termina con un retorno de carro seguido de un salto de línea, no con un salto de línea aislado. No escribirá estas secuencias manualmente, pero las encontrará en una captura de paquetes.
Para observar un par real, ejecute esto contra un sitio que administre:
curl -sS -o /dev/null -D - https://example.com/-D - escribe las cabeceras de respuesta en el terminal y -o /dev/null descarta el cuerpo. Prefiera esta opción a curl -I, porque -I envía una solicitud HEAD. Un servidor de aplicaciones que trata HEAD de forma distinta de GET, como hacen muchos, le mostrará entonces cabeceras que ningún navegador recibe. curl -v muestra ambos lados, con las líneas de solicitud marcadas con > y las líneas de respuesta con <.
Aspecto de la línea de solicitud en el registro de acceso de nginx
nginx incluye un formato de registro combined. Esta es su definición:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';Una línea generada por este formato:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45es$remote_addr, la dirección que abrió la conexión TCP (protocolo de control de transmisión). Detrás de un proxy, es la dirección del proxy, no la del visitante, a menos que haya configurado el módulo realip descrito más abajo para reemplazarla.- El primer
-es un marcador fijo. El segundo es$remote_user, que sólo se rellena cuando se usa la autenticación básica de HTTP. "GET /pricing HTTP/1.1"es$request, la línea de solicitud original completa. Con el escape de registros predeterminado de nginx, una comilla doble, una barra inversa y cualquier byte inferior a 32 o superior a 126 se escriben como\xXX. Por tanto, la línea es fiel, pero no siempre coincide byte a byte.200es el estado que devolvió su servidor, no el estado que percibió el visitante.5310es$body_bytes_sent, sólo el cuerpo. Las cabeceras de respuesta no se cuentan, por lo que este número siempre es menor que los bytes enviados realmente.- Los dos últimos campos entre comillas son
RefereryUser-Agent. Ambos proceden del cliente, por lo que pueden contener cualquier valor.
También se registran los datos no válidos, con escape. Un cliente que habla TLS (seguridad de la capa de transporte) con su puerto sin cifrar 80 deja una línea 400 cuyo campo de solicitud comienza con bytes escapados como "\x16\x03\x01\x02\x00\x01". \x16 es el tipo de registro del handshake TLS. Por tanto, esos bytes son el inicio de un ClientHello y no de una línea de solicitud. Su servidor funciona correctamente. Algo está dirigiendo HTTPS a un puerto HTTP.
Añada también $server_protocol al formato del registro. Imprime HTTP/1.1, HTTP/2.0 o HTTP/3.0. Es la forma más rápida de demostrar que un cambio de protocolo se aplicó realmente.
Qué significan los códigos de estado habituales cuando los devuelve tu propio sitio
El primer dígito indica la clase, y eso es lo primero que debes leer.
2xx significa que la operación funcionó. 200 OK se usa para una lectura normal. 201 Created se devuelve después de un POST que creó algo. 204 No Content indica que la operación tuvo éxito y que no hay nada que enviar de vuelta. Es la respuesta habitual a un DELETE.
3xx significa que debes buscar en otro lugar. 301 es permanente y los navegadores lo almacenan en caché durante mucho tiempo, a veces hasta que el usuario borra su perfil. Por eso, un 301 que apunta al nombre de host incorrecto puede ser difícil de deshacer. Usa 302 mientras sigues probando una redirección. 304 Not Modified indica un resultado correcto, no un error: el cliente envió una solicitud condicional, ya sea If-None-Match con un ETag (etiqueta de entidad) que todavía reconoces, o If-Modified-Since con una fecha desde la que el recurso no ha cambiado. Por eso respondiste con cabeceras y sin cuerpo. Un registro lleno de 304s indica que la caché funciona.
4xx significa que la solicitud era incorrecta. 400 Bad Request indica una entrada malformada. 401 Unauthorized significa realmente que el cliente no está autenticado y debe incluir una cabecera WWW-Authenticate que indique el esquema. 403 Forbidden significa que la solicitud se entendió, pero se rechazó de todos modos. 404 Not Found indica una ruta que no existe o cuya existencia el servidor prefiere no admitir, porque algunas aplicaciones responden con 404 en lugar de 403 para ocultar un recurso a los clientes que no tienen acceso. 405 Method Not Allowed indica que la ruta es correcta, pero el método no lo es. Es lo que devuelve un POST a una ubicación de archivos estáticos. 413 indica que el cuerpo supera client_max_body_size de nginx, cuyo valor predeterminado es 1 megabyte. El registro de errores lo confirma con client intended to send too large body.
Un 403 en un archivo estático casi siempre indica un problema del sistema de archivos y no una regla HTTP. Lee /var/log/nginx/error.log antes de cambiar la configuración. open() "/srv/site/index.html" failed (13: Permission denied) significa que el usuario de trabajo de nginx no puede leer el archivo. La causa más habitual es que un directorio padre no tenga permiso de ejecución para otros usuarios. directory index of "/srv/site/" is forbidden significa que la ruta resolvió a un directorio sin archivo de índice mientras autoindex está desactivado.
5xx significa que el problema está en tu lado. 500 es el error de servidor genérico y, en tu propio sitio, normalmente indica un error no gestionado en la aplicación. 502 Bad Gateway significa que nginx no pudo obtener una respuesta utilizable del upstream. El registro de errores indica la causa: connect() failed (111: Connection refused) while connecting to upstream significa que no hay ningún proceso escuchando en la dirección de proxy_pass. 504 Gateway Timeout significa que nginx dejó de esperar al upstream. El caso habitual es un upstream que aceptó la conexión y después no respondió dentro de proxy_read_timeout, cuyo valor predeterminado es 60 segundos. El registro lo anota como upstream timed out (110: Connection timed out) while reading response header from upstream. Un intento de conexión que no termina dentro de proxy_connect_timeout, también con un valor predeterminado de 60 segundos, termina igualmente en 504, y la línea del registro termina en while connecting to upstream. 503 Service Unavailable significa que el servidor no puede gestionar temporalmente la solicitud. Normalmente se debe a una sobrecarga o a tareas de mantenimiento programadas. Ten en cuenta que el limitador de tasa propio de nginx devuelve 503, porque limit_req_status tiene 503 como valor predeterminado. Si buscas 429 Too Many Requests en el registro y encuentras 503 en su lugar, esa es la razón. Configura limit_req_status 429; para obtener el código correcto.
Las cabeceras importantes al ejecutar el servidor
Host selecciona el sitio. Una sola dirección IP puede servir a cientos de nombres de host, y nginx compara Host con server_name para decidir qué bloque de server responde. Si no hay ninguna coincidencia, nginx usa el servidor predeterminado. Es el primer bloque que escucha en esa dirección y puerto, salvo que otro esté marcado con default_server. Si un nuevo host virtual devuelve el sitio equivocado, casi siempre se debe a esto: el nombre no coincidió y la petición terminó en el servidor predeterminado. Pruébelo sin modificar DNS:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent es una descripción del cliente escrita por el propio cliente y contiene texto libre. Úselo como indicio al revisar los registros. No lo use como mecanismo de control, porque un cliente que quiera falsearlo simplemente lo hará. Por tanto, bloquear un scraper mediante User-Agent sólo filtra a los que se identifican de forma correcta.
Content-Type determina cómo se interpretan los bytes: application/json para una petición de API y text/html; charset=utf-8 para una página. nginx asigna tipos a las extensiones de archivo mediante /etc/nginx/mime.types, y el nginx.conf incluido en los paquetes establece default_type application/octet-stream;. Por eso, un archivo cuya extensión nginx no conoce se ofrece como descarga en lugar de representarse. El síntoma visible es una página que carga sin estilos mientras la consola del navegador muestra Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME significa multipurpose internet mail extensions. Es el esquema de nombres del que proceden esas cadenas de tipos.
Cache-Control permite controlar todas las cachés entre el servidor y el lector. public, max-age=31536000, immutable es adecuado para recursos cuyo nombre contiene un hash del contenido, porque el nombre cambia cuando cambia el contenido. no-store debe usarse en cualquier contenido específico de un usuario, porque una caché compartida que conserve una página con la sesión iniciada se la entregará a la siguiente persona que solicite la misma URL. private es la opción intermedia: el navegador puede conservar el contenido, pero una caché compartida no.
X-Forwarded-For existe porque un proxy oculta al visitante. Cuando una petición pasa por un reverse proxy, $remote_addr es la dirección del proxy. Por tanto, los registros, la geolocalización y la limitación de tasa ven un único cliente hasta que se restaura la dirección original. El proxy debe reenviar la dirección original:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;Después, hay que indicar al servidor receptor que confíe en esa dirección y especificar exactamente en quién debe confiar:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;Estas dos directivas pertenecen al módulo realip de nginx. Sustituye $remote_addr por la dirección reenviada y conserva la dirección propia de la conexión en $realip_remote_addr. Si compila nginx manualmente, añada --with-http_realip_module, porque el módulo no se incluye de forma predeterminada.
Incluya únicamente rangos que controle. X-Forwarded-For es texto sin formato que cualquier cliente puede enviar, por lo que set_real_ip_from 0.0.0.0/0; permite que un visitante elija la dirección que se registra y la dirección que contabiliza el limitador de tasa.
X-Forwarded-Proto evita un fallo concreto y muy habitual. El proxy termina TLS y reenvía la petición a la aplicación mediante HTTP sin cifrar. La aplicación recibe una petición sin cifrar, decide que el visitante debe usar HTTPS y responde 301 https://example.com/. El navegador sigue la redirección, el proxy vuelve a terminar TLS y reenvía otra vez HTTP sin cifrar. El ciclo se repite hasta que el navegador lo abandona con ERR_TOO_MANY_REDIRECTS. Enviar X-Forwarded-Proto: https indica a la aplicación que el visitante ya está usando HTTPS, por lo que deja de redirigir.
HTTP/1.1 frente a HTTP/2 frente a HTTP/3: qué cambia para usted
HTTP/1.1 es texto y, en la práctica, gestiona una petición cada vez por conexión. Connection: keep-alive permite que la siguiente petición reutilice la misma conexión TCP, lo que evita el coste de establecerla de nuevo, pero las respuestas siguen llegando en el orden en que se solicitaron. El protocolo también permite el pipelining, mediante el que un cliente envía varias peticiones sin esperar las respuestas, pero la regla de orden se mantiene y ningún navegador moderno activa el pipelining de forma predeterminada. Una respuesta lenta bloquea todo lo que está en cola detrás. Eso es el bloqueo de cabecera de línea, y los navegadores lo evitan abriendo varias conexiones simultáneas al mismo nombre de host.
HTTP/2 conserva los mismos métodos y códigos de estado, pero cambia el formato de las tramas a binario. Muchas peticiones comparten una conexión como flujos independientes y el texto de las cabeceras repetidas se comprime, algo importante porque una petición moderna contiene muchas cabeceras. La conexión sigue usando TCP, por lo que un paquete perdido detiene todos los flujos de esa conexión hasta que llega la retransmisión. El bloqueo de cabecera de línea no desapareció. Bajó de HTTP a la capa de transporte. Server push formaba parte de HTTP/2 y, en la práctica, ya no se usa porque Chrome eliminó su compatibilidad en 2022.
HTTP/3 conserva de nuevo la misma semántica y sustituye TCP por QUIC, un transporte basado en UDP (user datagram protocol). Los flujos de QUIC son independientes hasta las capas inferiores, por lo que un paquete perdido sólo detiene el flujo al que pertenecía. TLS 1.3 está integrado en el establecimiento de conexión de QUIC, en lugar de funcionar como una capa superior, por lo que una conexión nueva necesita menos viajes de ida y vuelta. De aquí se derivan dos consecuencias prácticas: el puerto UDP que usa el servidor para HTTP/3 debe estar abierto en todos los firewalls del trayecto, y cualquier red que limite o bloquee UDP hará que los clientes vuelvan a HTTP/2. Ese puerto suele ser 443, pero el protocolo permite que el servidor use cualquier puerto UDP, porque el anuncio que dirige los navegadores a HTTP/3 siempre indica el puerto explícitamente.
Qué cambia para usted, en concreto. Normalmente, los navegadores no empiezan con HTTP/3. Se conectan mediante HTTP/2 o HTTP/1.1, reciben una cabecera Alt-Svc: h3=":443"; ma=86400 en la respuesta y usan HTTP/3 en las conexiones posteriores a ese host. La excepción es un registro HTTPS en DNS cuyo valor alpn incluye h3, lo que permite que un navegador como Firefox abra HTTP/3 en la primera conexión. Ese registro es una indicación adicional, no un sustituto, por lo que la cabecera no es un elemento opcional. Es el mecanismo principal de descubrimiento. En nginx, HTTP/2 pasó a tener su propia directiva en la versión 1.25.1 (http2 on; dentro del bloque server, en sustitución del parámetro antiguo listen ... http2), y QUIC llegó a la línea principal en 1.25.0, donde un sitio HTTP/3 necesita listen 443 quic reuseport; junto con el listen 443 ssl; habitual. El módulo HTTP/3 no se compila de forma predeterminada (--with-http_v3_module), y la documentación de nginx todavía considera experimental su compatibilidad.
Los proxies presentan distintos niveles de madurez en este aspecto, y conviene comprobarlo con la versión que realmente ejecuta. En noviembre de 2026, Caddy sirve HTTP/3 de forma predeterminada sin configuración adicional. nginx necesita el listener quic explícito y la cabecera Alt-Svc descrita arriba. Traefik lo activa por punto de entrada mediante una opción explícita http3. Si termina TLS en Traefik delante de varias aplicaciones Docker, allí se decide la versión del protocolo que reciben los visitantes, y el salto del proxy al contenedor suele ser HTTP/1.1 sin importar lo que haya negociado el navegador.
Verifique en lugar de darlo por hecho. curl --http3 -sS -o /dev/null -D - https://example.com/ sólo funciona si curl -V incluye HTTP3 entre sus características, y la mayoría de las compilaciones de las distribuciones no lo incluyen. La comprobación fiable es su propio registro: añada $server_protocol al formato y lea lo que negocian los navegadores reales. Antes de todo eso, confirme que el puerto UDP que usa su listener quic, 443 en la configuración anterior, está realmente abierto, porque un firewall que sólo permita TCP 443 hará que HTTP/3 falle silenciosamente mientras el sitio sigue funcionando mediante HTTP/2. Saber qué puertos están abiertos y escuchando en su servidor Linux es lo primero que debe comprobar.
HTTPS: HTTP es el protocolo y TLS es la capa de protección
HTTPS no es un protocolo independiente. Son las mismas solicitudes y los mismos códigos de estado dentro de una sesión TLS. El puerto 80 las transporta sin cifrar y el puerto 443 las transporta cifradas. El handshake de TLS se completa primero. Después, la solicitud HTTP viaja dentro del canal cifrado. Por eso un problema de certificado nunca tiene asociado un código de estado: el fallo ocurre antes de que se envíe un solo byte HTTP, así que no existe ninguna respuesta que numerar.
En un servidor que aloja varios sitios, importa un detalle del orden. El certificado se selecciona mediante SNI (indicación del nombre del servidor), un campo del handshake de TLS que transporta el nombre de host sin cifrar antes de que exista cualquier cabecera HTTP. Por tanto, el servidor selecciona primero un certificado a partir de SNI y después selecciona un host virtual a partir de la cabecera Host. Son dos búsquedas independientes que normalmente coinciden. Cuando no coinciden, el navegador muestra un error de nombre como NET::ERR_CERT_COMMON_NAME_INVALID y no envía ninguna solicitud, porque se ofreció el certificado del servidor predeterminado para un nombre que no cubre.
Para un sitio público, obtenga un certificado válido y configure su renovación automática. Certbot con Let's Encrypt en nginx escribe las rutas de los certificados en el bloque de servidor e instala el temporizador de renovación. Para un nombre de host que ninguna autoridad pública pueda validar, como un nombre interno o una dirección IP sin nombre en su propia red, un certificado autofirmado en Ubuntu es la opción adecuada, siempre que acepte que debe indicar a cada cliente que confíe en él.
Cuando TLS funcione, envíe todo lo que llegue al puerto 80 al puerto 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Añada Strict-Transport-Security sólo cuando esté seguro. La cabecera add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; indica a los navegadores que rechacen HTTP sin cifrar para ese nombre de host durante dos años. Los navegadores obedecen esta directiva desde su propia caché, por lo que eliminar después la cabecera no la revierte. Empiece con un max-age de unas horas. Confirme que todos los subdominios usan realmente HTTPS y después aumente el valor.
FAQ
¿Cuál es la diferencia entre HTTP y HTTPS?
HTTPS es HTTP transmitido dentro de una sesión TLS (transport layer security). Los métodos y los códigos de estado son idénticos. Lo que cambia es que los bytes se cifran entre el cliente y el componente que termina TLS, y el puerto predeterminado cambia de 80 a 443. Como el protocolo de enlace TLS termina antes de enviar el primer byte HTTP, un fallo del certificado nunca produce un código de estado. Por eso, una advertencia de certificado del navegador muestra un nombre de error como NET::ERR_CERT_COMMON_NAME_INVALID en lugar de un número como 403.
¿Por qué mi sitio devuelve 502 Bad Gateway?
Un 502 de nginx significa que nginx no pudo obtener una respuesta utilizable del upstream al que hace proxy. Por tanto, la petición del visitante era correcta y el problema estaba detrás de nginx. Lea /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream significa que no hay ningún proceso escuchando en la dirección y el puerto de proxy_pass. Compruebe que la aplicación esté en ejecución y vinculada a la dirección esperada. no live upstreams while connecting to upstream significa que todos los servidores del bloque upstream se marcaron como no disponibles después de varios fallos. Compárelo con 504 Gateway Timeout, que significa que nginx agotó el tiempo de espera mientras esperaba al upstream, ya fuera para completar la conexión dentro de proxy_connect_timeout o para recibir una respuesta dentro de proxy_read_timeout.
¿Por qué mi registro de acceso muestra la misma dirección IP para todos los visitantes?
Porque $remote_addr registra la dirección que abrió la conexión TCP. Detrás de un reverse proxy o de una red de distribución de contenido, esa dirección es la del proxy. La dirección del visitante llega en la cabecera X-Forwarded-For. Configure proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; en el proxy. Después, en el nginx receptor, configure set_real_ip_from con el rango de direcciones del proxy y real_ip_header X-Forwarded-For;. Incluya sólo rangos que controle. Esa cabecera es texto que cualquier cliente puede enviar. Si confía en ella desde toda Internet, un visitante podrá elegir la dirección que registra y la dirección que se usa para limitar la tasa.
¿Necesito activar HTTP/2 o HTTP/3?
Conviene activar HTTP/2. Es una directiva en un sitio que ya tiene TLS y elimina el límite de peticiones por conexión que ralentiza las páginas con muchos archivos pequeños. HTTP/3 ofrece una mejora menor y menos predecible. Además, requiere un puerto UDP abierto, normalmente 443, y una compilación del proxy con compatibilidad con QUIC. Recuerde que los navegadores normalmente cambian a HTTP/3 sólo después de recibir una cabecera Alt-Svc en una respuesta anterior o un registro DNS HTTPS que incluya h3. Sin uno de esos elementos, nada cambia, independientemente de lo que indique la línea listen. Añada $server_protocol al formato del registro y mida el protocolo que negocian realmente los visitantes antes de dedicarle tiempo.
¿Qué significa 403 Forbidden cuando el archivo existe?
En un sitio estático, un 403 suele deberse a permisos del sistema de archivos y no a una regla HTTP. open() ... failed (13: Permission denied) en /var/log/nginx/error.log significa que el usuario de los workers de nginx no puede leer el archivo. Lo más habitual es que un directorio padre no tenga permiso de ejecución para otros usuarios, no que el modo del archivo sea incorrecto. directory index of ... is forbidden significa que la petición se resolvió en un directorio sin archivo de índice mientras autoindex está desactivado. Una regla deny explícita en el bloque location correspondiente también devuelve 403. Revise ese bloque cuando el registro de errores no muestre nada.