Volver al blog

Síguenos y suscríbete

Recomendaciones sobre el uso del encabezado Vary

Rogier Mulhuijzen

Senior Professional Services Engineer

¿Qué es Vary?

Vary figura entre los encabezados de respuesta HTTP más potentes. Si se utiliza de forma correcta, permite hacer cosas extraordinarias. Sin embargo, se suele usar incorrectamente, lo cual puede producir pésimas tasas de resultados. Y, lo que es peor aún, si no se utiliza en el momento conveniente, se podría distribuir contenido equivocado.

En lugar de remitirte a las especificaciones de Vary, voy a explicar el encabezado Vary recurriendo al caso de uso más habitual: la compresión.

Si utilizas mod_deflate de Apache, el encabezado Vary correcto se añade automáticamente a tus respuestas. El mismo principio se aplica a la función gzip de Fastly. Sin embargo, dado que se trata de un uso de Vary muy sencillo y habitual, voy a utilizarlo para exponer de qué modo funciona Vary.

undefined

Anatomía de una petición HTTP

Normalmente, cada vez que una petición accede a una de las memorias caché de Fastly, solo se utilizan dos secciones de dicha petición para encontrar un objeto en la caché: la ruta de acceso (y la cadena de consulta, si la hubiera) y el encabezado Host.

Esta es una petición bastante habitual para http://example.com/somepage.php:

GET **/somepage.php** HTTP/1.1
Host: **example.com**
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36
Accept-Language: en-US,en;q=0.8
Accept-Encoding: gzip,deflate,sdch

Como acabamos de ver, el navegador envía un montón de información junto con la URL. El encabezado Accept te indica qué tipo de contenido prefiere el navegador; User-Agent especifica de qué navegador se trata y la versión de este; Accept-Language recoge una lista de idiomas (y dialectos) que ha configurado el usuario, y Accept-Encoding muestra con qué modelos de compresión es compatible el navegador.

Por razones prácticas, solo nos ocuparemos de gzip, ya que deflate es antiguo, y sdch solo lo utiliza Google.

Funcionamiento de la compresión

Supongamos que tienes un servidor web sin mod_deflate, pero que has descubierto cómo hacer compresión gzip con PHP. Así que, cuando ves gzip en el encabezado Accept-Encoding, defines el encabezado Content-Encoding: gzip de modo que indique al navegador lo que estás haciendo, y comprimes el cuerpo de la respuesta.

Imaginemos, además, que este servidor fuera el origen de un servicio de Fastly y que esta página es la que podemos almacenar en caché. ¿Qué ocurriría si un navegador que no entiende la comprensión es el primero en solicitar esta página? La página descomprimida acabaría en nuestra caché.

¿Es esto un problema? Relativamente. Si un navegador que verdaderamente entiende la compresión solicita esta página, consigue de nuestra caché la versión descomprimida y la presenta sin problemas. Sin embargo, la versión descomprimida tiene mayor tamaño, de modo que su transmisión necesitará más ancho de banda, lo cual se traduce en una entrega más lenta para el usuario final y en un encarecimiento de los costes para ti.

El problema más serio llega cuando la primera petición por la que se requiere un objeto procede de un navegador que realmente recoge funciones de compresión: la versión comprimida acaba en nuestra caché. Así pues, cuando aparece un navegador que no entiende la comprensión, este obtiene la versión comprimida, pero no tiene ni idea de qué hacer aparte de mostrar un galimatías.

Uso de Vary para solucionar problemas

Este problema tiene dos soluciones. La primera te permite cambiar la clave de caché con tu configuración de Fastly, aunque eso a su vez suele ocasionar problemas adicionales:

  1. Tendrías que purgar las versiones comprimida y descomprimida por separado.

  2. Cualquier error cometido en este punto podría provocar que todas las URL devolvieran el mismo objeto.

Para evitar ambos problemas, puedes utilizar el encabezado Vary.

El encabezado Vary indica a cualquier caché HTTP qué secciones del encabezado de la petición, que no sean la ruta de acceso ni el encabezado Host, han de tenerse cuenta al tratar de encontrar el objeto adecuado. Lo hace enumerando los nombres de los encabezados pertinentes, que en este caso es Accept-Encoding. Si son varios los encabezados que influyen en la respuesta, quedarán listados en un único encabezado, separados por una coma.

Los encabezados de respuesta correspondientes a una respuesta comprimida deberían tener este aspecto:

HTTP/1.1 200 OK
Content-Length: 3458
Cache-Control: max-age=86400
Content-Encoding: gzip
**Vary: Accept-Encoding**

Y, en cuanto a una respuesta descomprimida, los encabezados de respuesta deberían tener este aspecto:

HTTP/1.1 200 OK
Content-Length: 8365
Cache-Control: max-age=86400
**Vary: Accept-Encoding**

Debes tener en cuenta que el encabezado Vary está presente en la respuesta, con independencia de si se utiliza la compresión o no.

¿Y esto por qué sucede? Vamos a examinar qué ocurre cuando el encabezado está realmente ahí.

Todo comienza con la llegada de una petición que requiere un objeto que carece del encabezado Accept-Encoding. Dado que el objeto no está en la caché, lo solicitamos al origen, que lo devuelve con el encabezado Vary. Al almacenar Fastly el objeto en la caché, el encabezado Vary queda registrado, y los valores de los encabezados pertinentes correspondientes a la petición se almacenan también.

Así pues, en la caché hay un objeto que tiene un pequeño indicador que dice: «Para uso exclusivo con peticiones que no tengan Accept-Encoding».

Ahora pongamos que aparece un navegador que realmente entiende la compresión y envía la petición tal y como se ha descrito antes. En primer lugar, la buscamos en función del encabezado Host y la ruta de acceso. De esta forma se encontrará el objeto, pero la petición tiene un encabezado Accept-Encoding que está definido como gzip,deflate,sdch, lo cual no coincide con el indicador que lleva aparejado este objeto.

Así que Fastly vuelve a tu origen, y esta vez deberíamos obtener una versión comprimida del objeto. Esta respuesta se almacena con un indicador en el que se afirma que esta versión únicamente debe utilizarse con peticiones que tengan un encabezado Accept-Encoding: gzip,deflate,sdch.

Si el encabezado Vary no hubiera estado ahí en la primera respuesta, no habríamos sabido que no podíamos utilizar el objeto almacenado en caché para la segunda petición.

Normalización

Quizás te preguntes si todos los navegadores actuales envían el mismo encabezado Accept-Encoding.

Desafortunadamente, la respuesta es no.

He muestreado 100 000 peticiones en una de nuestras cachés y he obtenido 44 encabezados Accept-Encoding diferentes. (Si te interesa saber cómo lo hice, o las cifras, consulta este otro artículo).

Si todas esas peticiones hubieran ido destinadas a la misma URL, nuestra caché habría recogido 44 versiones «distintas». Pero, dado que el origen solo puede generar dos versiones —una si gzip está presente en Accept-Encoding, y otra si no lo está—, eso equivale a 42 peticiones con destino al origen que nos interesaría evitar.

Ya que el origen solamente se ocupa de si gzip está presente ahí o no, ¿por qué no normalizamos el encabezado Accept-Encoding de modo que o bien contenga gzip o bien no esté presente en absoluto?

En todo momento hay dos únicas variaciones del encabezado Accept-Encoding en nuestras peticiones y, por tanto, únicamente tendremos dos variaciones del objeto en nuestra caché.

Esto se hace fácilmente con un poco de código VCL:

# do this only once per request
if (req.restarts == 0) {
  # normalize Accept-Encoding to reduce vary
  if (req.http.Accept-Encoding ~ "gzip") {
    set req.http.Accept-Encoding = "gzip";
  } else {
    unset req.http.Accept-Encoding;
  }
}

A pesar de todo, es posible que te interese tener compatibilidad con algunos clientes HTTP antiguos, por lo que vamos a agregar también compatibilidad con deflate y vamos a asegurarnos de que Internet Explorer 6 no tenga que hacer frente a la compresión (es de sobra conocido su pésimo rendimiento en este sentido).

# do this only once per request
if (req.restarts == 0) {
  # normalize Accept-Encoding to reduce vary
  if (req.http.Accept-Encoding) {
    if (req.http.User-Agent ~ "MSIE 6") {
      unset req.http.Accept-Encoding;
    } elsif (req.http.Accept-Encoding ~ "gzip") {
      set req.http.Accept-Encoding = "gzip";
    } elsif (req.http.Accept-Encoding ~ "deflate") {
      set req.http.Accept-Encoding = "deflate";
    } else {
      unset req.http.Accept-Encoding;
    }
  }
}

Desde la fundación de nuestra empresa, un fragmento de código VCL misteriosamente parecido a este ha formado parte del código maestro VCL de Fastly.

Sugerencias sobre el uso de Vary

Como puedes ver, la mera incorporación de un sencillo encabezado Vary a tu respuesta, sin normalizar un poco los encabezados de la petición, podría haber tenido unas consecuencias bastante desastrosas sobre el volumen de peticiones enviadas al origen. El único factor que lo impide es la normalización de serie que realiza Fastly.

En primer lugar, sin normalización no te interesa añadir variaciones con respecto a un encabezado que tiene ya de por sí muchas variaciones.

En segundo lugar, al proceder a la normalización, trata de reducir las posibilidades de encabezado a unas pocas como mucho. Un método para conseguirlo es procurar que los valores estén codificados de forma rígida en tu código VCL en lugar de depender de regsub(). Una norma general que no falla: si tienes contenido popular cuya fecha de caducidad esté fijada para un futuro lejano, el volumen de tráfico destinado a tu origen se amplía de forma lineal con respecto a la cantidad de posibles valores.

A continuación te mostraré algunos encabezados Vary en los que me he fijado con el paso del tiempo y te explicaré por qué son malos y cómo normalizarlos.

Vary: User-Agent

No exagero al afirmar que hay miles de cadenas User-Agent diferentes. En una muestra de 100 000 peticiones, he encontrado alrededor de 8000 cadenas distintas.

En algunas situaciones hipotéticas, te interesa presentar diferentes formatos a los usuarios móviles. Ten en cuenta que, según este ejemplo, el encabezado User-Agent es reemplazado con una sencilla cadena que en absoluto se parece a los valores habituales de este encabezado. Si lo utilizas, asegúrate de que tu origen sepa cómo gestionarlo.

Podrías utilizar un fragmento de código VCL como este:

if (req.http.User-Agent ~ "(Mobile|Android|iPhone|iPad)") {
  set req.http.User-Agent = "mobile";
} else {
  set req.http.User-Agent = "desktop";
}

Vary: Referer

Si tu contenido es muy frecuentado, lo más probable es que muchos otros sitios establezcan enlaces a este, y cada una de las consultas de búsqueda de Google presente una URL única. Ambos factores a menudo se traducen en un elevado número de valores Referer extraordinarios.

Pongamos que quieres mostrar un tipo de mensaje emergente para dar la bienvenida a quienes accedan a una de tus páginas desde una página ajena a tu sitio web, pero no quieres que se muestre a quienes ya estén navegando por el sitio web.

El código VCL que necesitas tendría este aspecto:

if (req.http.Referer ~ "^https?://www.example.com/") {
  set req.http.Referer = "http://www.example.com/";
} else {
  unset req.http.Referer;
}

Vary: Cookie

Cookie probablemente sea uno de los encabezados de peticiones más extraordinarios que exista, lo que no es una ventaja en absoluto. Las cookies suelen ser portadoras de datos de autenticación, en cuyo caso te conviene no tratar de almacenar páginas en caché, sino obviarlas. Si te interesa el almacenamiento en caché de cookies de rastreo, obtendrás más información en este otro artículo.

Sin embargo, en ocasiones las cookies se utilizan para hacer test A/B, en cuyo caso es una buena idea añadir variaciones respecto de un encabezado adaptado y dejar intacto el encabezado Cookie. Se evita así mucha lógica adicional con la que asegurarse de que el encabezado Cookie se reserve para las URL que lo necesiten (y que probablemente no puedan almacenarse en caché).

sub vcl_recv {
  # set the custom header
  if (req.http.Cookie ~ "ABtesting=B") {
    set req.http.X-ABtesting = "B";
  } else {
    set req.http.X-ABtesting = "A";
  }
...
}

...

sub vcl_fetch {
  # vary on the custom header
  if (beresp.http.Vary) {
    set beresp.http.Vary = beresp.http.Vary ", X-ABtesting";
  } else {
    set beresp.http.Vary = "X-ABtesting";
  }
  ...
}

...

sub vcl_deliver {
  # Hide the existence of the header from downstream
  if (resp.http.Vary) {
    set resp.http.Vary = regsub(resp.http.Vary, "X-ABtesting", "Cookie");
  }
  # Set the ABtesting cookie if not present in the request
  if (req.http.Cookie !~ "ABtesting=") {
    # 75% A, 25% B
    if (randombool(3, 4)) {
      add resp.http.Set-Cookie = "ABtesting=A; expires=" now + 180d "; path=/;";
    } else {
      add resp.http.Set-Cookie = "ABtesting=B; expires=" now + 180d "; path=/;";
    }
  }

  ...
}

En vcl_recv, que precede a la búsqueda en la caché, utilizas código VCL para añadir un encabezado adaptado que se basa en el encabezado Cookie. Aquí se presupone que el valor de la cookie es B o A y que, si la cookie falta, queda definida de manera predeterminada como A.

A continuación, en vcl_fetch, añades al encabezado Vary tu encabezado adaptado. Y, por último, en vcl_deliver, sustituyes el encabezado adaptado del encabezado Vary con el encabezado Cookie. De este modo, no solo se ocultará al exterior la existencia del encabezado adaptado, sino que además, si da la casualidad de que hubiera una caché compartida entre Fastly y los usuarios finales, estos seguirán recibiendo la variación correcta.

Y ahora, turno para el test A/B en el que lo único que ven los navegadores es otra cookie, y lo único que tiene que hacer tu origen es algo diferente a partir de un encabezado muy sencillo (X-ABtesting).

Vary: *

No utilices este encabezado.

La norma RFC sobre HTTP indica que si un encabezado Vary contiene el nombre de encabezado especial *, cada una de las peticiones destinadas a dicha URL ha de ser tratada supuestamente como una petición única (y no almacenable en caché).

Esto se señala mucho mejor mediante Cache-Control: private, que resulta más claro para quien lea los encabezados de respuesta. También quiere decir que el objeto en ningún caso debe almacenarse, práctica que conlleva un mayor grado de seguridad.

Vary: Accept-Encoding, User-Agent, Cookie, Referer

No es una broma: he visto encabezados así, sin normalización alguna. Como habrás adivinado, quizás haya una probabilidad entre un gúgolplex de que este encabezado reciba un acierto de caché.

Siguiente nivel

Hasta el momento, he analizado Vary en cuanto a la compresión, he presentado un poco de lógica (sencilla) destinada a la detección de dispositivos y he configurado una lógica de realización de test A/B en el borde. En mi próximo artículo profundizaré en usos más avanzados de Vary, como la geolocalización por IP y Accept-Language.