# 103 Early Hints

> Deja que el navegador empiece a cargar hojas de estilo, fuentes y la imagen principal mientras tu origen genera la página, con enlaces que el borde aprende.

Con Early Hints activado, el borde responde a una navegación HTML con un `103 Early Hints` **antes** de pedir la página a tu origen. El `103` lleva cabeceras `Link` (`rel=preload`, `preconnect`, `modulepreload`) con los recursos críticos de la página, así que el navegador los descarga mientras el origen aún genera el HTML. Mejora el LCP de las páginas que tardan en el origen y no cambia la respuesta final.

Early Hints es independiente de la caché: puedes usarlo con la caché activada o desactivada. Viene **desactivado por defecto**.

## Cuándo envía el borde un 103

Solo si el sitio tiene Early Hints activado y la petición:

- es una navegación `GET` (`Accept` contiene `text/html`), sin `Range`, por HTTP/1.1 o HTTP/2;
- **va al origen**: un `MISS` o `EXPIRED` de caché, una petición `BYPASS` o `DYNAMIC`, o un sitio sin caché;
- es de una página para la que el nodo del borde ya aprendió enlaces.

Nunca en un `HIT` ni en un `UPDATING` de caché (la respuesta ya es inmediata), en un reto, en la puerta de acceso, en un bloqueo del WAF, en una redirección del borde ni en una petición `HEAD`.

## Cómo aprende los enlaces

Cada nodo del borde aprende los enlaces por host y ruta (sin la consulta) a partir de las respuestas `200` `text/html` de tu origen. La primera visita a una página enseña al nodo y las siguientes reciben las pistas. Lo aprendido se guarda en memoria una hora y se renueva con cada `200`.

| `mode` | Panel | Fuente |
|---|---|---|
| `origin_links` | Cabeceras Link del origen | Las cabeceras `Link` del origen con `rel=preload`, `preconnect` o `modulepreload`. |
| `html_head` | Head del HTML | Los primeros 32 KB del HTML: `<link rel=preload \| modulepreload \| preconnect \| stylesheet>` (una hoja de estilos se anuncia como `preload; as=style`) e `<img fetchpriority=high>` (como `preload; as=image`, con `imagesrcset`/`imagesizes`). |
| `both` (por defecto) | Ambos | Las dos fuentes juntas. |

Solo se anuncian enlaces `https` del propio origen de la página o de uno de los **orígenes adicionales**, sin credenciales en la URL; como mucho 10 por página. Primero van las hojas de estilo y las imágenes LCP, después las fuentes y `preconnect` y por último los scripts.

> **Nota: Privacidad**
>
> El borde nunca aprende de una petición con `Authorization`. Una petición con `Cookie` solo enseña si la respuesta es compartible: sin `Set-Cookie`, sin `Cache-Control: private` ni `no-store`, sin `Vary: Cookie` ni `Vary: *`. Así, un `103` nunca anuncia recursos de la sesión de otro usuario.


## Actívalo

**Panel**

En **Caché › Early Hints (103)**, activa **Enviar Early Hints**, elige **De dónde aprender los enlaces** y, si tus recursos vienen de una CDN o de un servidor de fuentes, añádelos en **Orígenes adicionales**. Pulsa **Guardar cambios**.

**CLI**

```sh
openwaf-cloud ctl zones set example.com early_hints.enabled:=true --reason "103 Early Hints"
openwaf-cloud ctl zones set example.com early_hints.mode=html_head 'early_hints.extra_origins:=["https://cdn.example.com"]' --reason "CDN"
# Desactivado para un nombre:
openwaf-cloud ctl zones set example.com --host api.example.com early_hints.enabled:=false --reason "API sin HTML"
```

**API**

```json
{"early_hints": {"enabled": true, "mode": "both", "extra_origins": ["https://cdn.example.com"]}}
```

Se envía con `PATCH /v1/orgs/{org}/zones/{zone}/edge`. El parche se combina campo a campo (`{"early_hints": {"mode": "html_head"}}` solo cambia el modo); `extra_origins` se sustituye entera y `"early_hints": null` vuelve a los valores por defecto.

**MCP**

`update_settings` con `{"zone": "example.com", "settings": {"early_hints": {"enabled": true}}, "reason": "103 Early Hints"}`; añade `host` para un nombre.


## Ajustes

| Clave | Valores | Por defecto |
|---|---|---|
| `early_hints.enabled` | `true`, `false` | `false` |
| `early_hints.mode` | `origin_links`, `html_head`, `both` | `both` |
| `early_hints.extra_origins` | Hasta 10 orígenes `https://host` o `https://host:puerto`, sin ruta | `[]` |

Los orígenes adicionales se normalizan (minúsculas, sin `/` final, sin `:443`); un origen que no sea https, con ruta, con comodín o repetido se rechaza con un `422`.

**Por nombre**, `hosts.<nombre>.early_hints` sustituye **entero** al ajuste de la zona. Los campos que no indiques se copian de la zona al crear el ajuste propio, así que `{"enabled": false}` solo desactiva ese nombre; `"early_hints": null` dentro del nombre vuelve a heredar el de la zona.

## Compruébalo

Pide una página dos veces como un navegador; la segunda vez el nodo ya la ha aprendido:

```sh
curl -sv --http2 -o /dev/null https://www.example.com/ -H 'Accept: text/html' 2>&1 | grep -E '^< (HTTP|link)'
# < HTTP/2 103
# < link: </style.css>; rel=preload; as=style
# < HTTP/2 200
```

¿No aparece el `103`? La página salió de la caché (`x-cache-status: HIT`; prueba una ruta que no se cachee), aún no se vio un `200` HTML compartible o la petición llegó a otro nodo del borde que todavía no ha aprendido la página. Lo aprendido se queda en cada nodo y no se muestra en el panel.
