La Edge Cloud Platform de Fastly

Back to blog

Follow and Subscribe

Recomendaciones sobre el uso del encabezado Vary

Rogier Mulhuijzen

Ingeniero sénior de servicios profesionales

¿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, si se usa incorrectamente, 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 agrega 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.

error no definido

Anatomía de una solicitud HTTP

Normalmente, cada vez que una solicitud accede a una de las memorias caché de Fastly, solo se utilizan dos secciones de dicha solicitud 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 solicitud 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 demasiada información junto con la URL. El encabezado Accept indica qué tipo de contenido prefiere el navegador; User-Agent especifica de qué navegador se trata y la versión de este; Accept-Language recopila 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 compresió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 costos para ti.

El problema más serio llega cuando la primera solicitud por la que se requiere un objeto procede de un navegador que recopila funciones de compresión: la versión comprimida acaba en nuestra caché. Así, cuando aparece un navegador que no entiende la compresión, este obtiene la versión comprimida, pero no tiene idea de qué hacer aparte de mostrar un lío.

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 depurar 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 solicitud, que no sean la ruta de acceso ni el encabezado Host, deben 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 verse así:

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 verse así:

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, independientemente 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 solicitud 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 solicitud también se almacenan.

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

Ahora pongamos que aparece un navegador que realmente entiende la compresión y envía la solicitud tal 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 solicitud tiene un encabezado Accept-Encoding que está definido como gzip,deflate,sdch, lo cual no coincide con el indicador que lleva este objeto de manera inherente.

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 solicitudes 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 solicitud.

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 solicitudes en una de nuestras cachés y 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 solicitudes hubieran ido destinadas a la misma URL, nuestra caché habría recopilado 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 solicitudes 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 contenga gzip o no esté presente en absoluto?

En todo momento hay dos únicas variaciones del encabezado Accept-Encoding en nuestras solicitudes 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 muy 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 solicitud, podría haber tenido consecuencias bastante desastrosas sobre el volumen de solicitudes 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 agregar 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 deficientes 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 solicitudes, 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 no se parece en nada 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 tiene muchas visitas, 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 se vería así:

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 solicitudes 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 evitarlas. 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 una prueba A/B, en cuyo caso es buena idea agregar 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 agregar 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, agregas 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 casualmente hubiera una caché compartida entre Fastly y los usuarios finales, estos seguirán recibiendo la variación correcta.

Y ahora, es el momento de la prueba 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 solicitudes destinadas a dicha URL debe tratarse supuestamente como una solicitud ú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

Es en serio: 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, presentado un poco de lógica (sencilla) destinada a la detección de dispositivos y configurado una lógica de realización de una prueba A/B en el edge. En mi próximo artículo profundizaré en usos más avanzados de Vary, como la geolocalización por IP y Accept-Language.

¿Listo para empezar?

Ponte en contacto con nosotros