Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 018 — Negociación de contenido#

⬅️ 017 · 📚 Parte 1 · 🎓 Clases · 019 ➡️ Parte 1 — Responder · Nivel 🟡 intermedio · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Servir la representación que el cliente pide, y declararlo para que ninguna caché sirva la equivocada.

🧩 La situación#

GET /tareas/1 devuelve JSON si el cliente pide application/json y HTML si pide text/html. Si pide algo que no sabes servir, 406. Y en todos los casos la respuesta lleva Vary: Accept.

📖 Por qué Vary no es opcional#

Una caché guarda respuestas indexadas por URL. Si dos clientes piden /tareas/1 y uno quiere JSON y el otro HTML, la caché serviría al segundo lo que guardó del primero.

Vary: Accept le dice a la caché que esa URL tiene varias representaciones y que debe guardarlas por separado según la cabecera Accept [rfc9111]. Sin ella, la negociación funciona en desarrollo —donde no hay caché— y falla en producción.

Es un fallo especialmente desagradable porque es intermitente: depende de quién pidió primero.

🧮 El contrato#

Petición Respuesta
accept: application/json 200 · content-type: application/json
accept: text/html 200 · content-type: text/html
cualquiera vary contiene accept
accept: application/pdf 406
accept: */* 200 · JSON (la representación por omisión)

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Negociación de contenido Que el mismo recurso se sirva en el formato que el cliente prefiere, según su cabecera Accept. Obliga a emitir Vary: Accept: sin ella una caché serviría el HTML a quien pidió JSON, porque para ella las dos peticiones son la misma URL.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-018-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase018.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Cuatro frameworks y cuatro repartos distintos del mismo trabajo: elegir el tipo, emitir Vary y decidir el 406. Ninguno hace las tres cosas.

Express · express/server.mjs — las tres, hechas#

  respuesta.format({
    "application/json": () => respuesta.json(tarea),
    "text/html": () => respuesta.type("text/html").send(`<h1>${tarea.titulo}</h1>`),
    default: () => respuesta.status(406).json({ error: "no puedo servir ese tipo" }),
  });

format hace tres cosas a la vez: elige según Accept, emite Vary: Accept por su cuenta y llama a default cuando nada encaja. Es de las pocas veces en que Express trae resuelto algo no trivial, y la única del elenco donde el Vary no hay que acordarse de poner.

Ese detalle importa más de lo que parece: sin Vary: Accept, una caché intermedia serviría el HTML a quien pidió JSON, porque para ella las dos peticiones son la misma URL [rfc9111].

FastAPI · fastapi/main.py — ninguna, y por diseño#

def preferido(accept: str) -> str | None:
    candidatos = []
    for parte in accept.split(","):
        trozos = parte.split(";")
        tipo = trozos[0].strip()
        calidad = 1.0
        for extra in trozos[1:]:
            if extra.strip().startswith("q="):
                try:
                    calidad = float(extra.strip()[2:])
                except ValueError:
                    calidad = 0.0
        if calidad > 0:
            candidatos.append((calidad, tipo))
    candidatos.sort(key=lambda x: -x[0])

Starlette no negocia. Expone la cabecera y deja la decisión a la aplicación, así que esta implementación es la más larga con diferencia: hay que analizar los valores de calidad (q=), ordenarlos y resolver los comodines a mano.

Léela entera aunque sea larga, porque es la única que enseña qué hay dentro de la negociación. Lo que en Express es una llamada, aquí son veinte líneas — y las veinte están en el estándar [rfc9110].

    cabeceras = {"vary": "Accept"}

Y el Vary también a mano, en las tres ramas.

Spring Boot · spring-boot/…/Aplicacion.java — la elección y el 406#

    @GetMapping(value = "/tareas/1", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, String>> json() {
        return ResponseEntity.ok().header("Vary", "Accept")
                .body(Map.of("id", "1", "titulo", "negociar"));
    }
    @GetMapping(value = "/tareas/1", produces = MediaType.TEXT_HTML_VALUE)
    public ResponseEntity<String> html() {
        return ResponseEntity.ok().header("Vary", "Accept").body("<h1>negociar</h1>");
    }

El enfoque más declarativo de los cuatro, y el único donde la negociación no aparece como código: dos métodos con la misma ruta y distinto produces. Cada uno declara qué sabe servir y Spring elige; si ninguno encaja, emite el 406 por su cuenta.

La consecuencia práctica es que añadir un tercer formato es añadir un método, no tocar una condición. El Vary, en cambio, hay que ponerlo — y hay que ponerlo en los dos.

ASP.NET Core · aspnet-core/Program.cs — decisión explícita#

    respuesta.Headers.Vary = "Accept";
    var accept = peticion.Headers.Accept.ToString();

    if (accept.Contains("application/json") || accept.Contains("*/*") || accept.Length == 0)
    {
        return Results.Json(new { id = "1", titulo = "negociar" });
    }
    if (accept.Contains("text/html"))
    {
        return Results.Content("<h1>negociar</h1>", "text/html");
    }
    return Results.Json(new { error = "no puedo servir ese tipo" }, statusCode: 406);

Las API mínimas no negocian: la decisión es un if. Y conviene declarar la limitación de este extracto, porque es una comparación honesta y no un juicio sobre el framework: los controladores MVC de ASP.NET Core sí tienen formateadores de salida configurables y negocian como Spring. Lo que se compara aquí es el camino mínimo, que es el que se elige para un servicio pequeño.

Fíjate también en que accept.Contains(...) ignora los valores de calidad. Funciona para el contrato de esta clase y no implementa el estándar: un Accept: text/html;q=0.9, application/json;q=0.1 elegiría JSON si aparece primero en la condición. Es exactamente el trabajo que FastAPI hace a mano y que Express y Spring hacen por ti.

🔬 Comparación#

Framework ¿Negocia solo? ¿Vary automático? ¿406 automático?
Express , con format sí, con default
Spring Boot , con produces no
FastAPI no no no
ASP.NET Core no en API mínimas no no

Dos de cuatro negocian y solo uno emite Vary por su cuenta. Es la cabecera que más se olvida del programa, y la que produce el fallo más difícil de reproducir.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 018

🧪 Reto de transferencia#

Añade text/csv como tercera representación y comprueba qué implementación necesita menos cambios. La respuesta te dice cuál escala mejor cuando el número de formatos crece.

🔗 Enlaces#

Fuentes#