Categorías: Labs

¡Esta API es tan fascinante!

Esta es una traducción del artículo original publicado en el blog de Mozilla Hacks.

Por más de una década la Web ha usado XMLHttpRequest (XHR) to lograr peticiones asíncronas en JavaScript. A pesar que XHR es muy útil, XHR no es una muy buena API. Carece la falta de separación de responsabilidades. La entrada, salida y estado están todas gestionadas interactuando con un objeto, y el estado está registrado usando eventos. También, el modelo basado en eventos tampoco es bueno con el reciente enfoque en promesas (Promise) y programación asíncrona basada en generadores.

La API Fetch intenta corregir estos problemas. Introduciendo las mismas premisas a JavaScript que son usadas en el protocolo HTTP. Además, introduce una función de utilidad fetch() que brevemente captura la intención de obtener un recurso desde la red.

La especificación de Fetch, que define la API, establece la semántica de un agente de usuario obteniendo un recurso. Esto, combinado con ServiceWorkers, es un intento para:

Al escribir este artículo, la API Fetch está disponible en Firefox 39 y Chrome 42. Github tiene un polyfill para Fetch.

Detección de características

El soporte para Fetch API puede ser detectado buscando Headers, Request, Response o fetch en el contexto de window o worker.

Obtención simple

La parte más útil y alto nivel de la API Fetch es la función fetch(). En su forma más simple, toma una URL y retorna una promesa que resuelve la respuesta. La respuesta es capturada como un objeto Response.

fetch("/data.json").then(function(res) {
    // res instanceof Response == true.
    if (res.ok) {
        res.json().then(function(data) {
            console.log(data.entries);
        });
    } else {
        console.log("Parece que la respuesta no fue correcta, obtuvo el estado", res.status);
    }
}, function(e) {
    console.log("Obtención fallida", e);
});

Enviando algunos parámetros, puede verse algo así:

fetch("http://www.example.org/submit.php", {
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded"
    },
    body: "firstName=Nikhil&favColor=blue&password=easytoguess"
}).then(function(res) {
    if (res.ok) {
        alert("Perfecto! Tu configuración está guardada.");
    } else if (res.status == 401) {
        alert("Oops! No estás autorizado.");
    }
}, function(e) {
    alert("Error enviando el formulario!");
});

Los argumentos de la función fetch() son los mismos que los pasados en el constructor Request(), así que puedes pasar directamente peticiones arbitrariamente complejas a fetch() como lo veremos adelante.

Cabeceras

Fetch introduce 3 interfaces. Estas son Headers, Request y Response. Estas mapean directamente al concepto de las subcapas de HTTP, pero tienen ciertos filtros de visibilidad por razones de privacidad y seguridad, como soporte a reglas CORS y asegurar que las cookies no sean legibles por terceras partes.

La interfaz Header es un simple multi mapa de nombres y valores:

var content = "Hola, Mundo";
var reqHeaders = new Headers();
reqHeaders.append("Content-Type", "text/plain"
reqHeaders.append("Content-Length", content.length.toString());
reqHeaders.append("X-Custom-Header", "ProcessThisImmediately");

Lo mismo puede ser logrado pasando un array de arrays o un objeto literal de JS al constructor:

reqHeaders = new Headers({
    "Content-Type": "text/plain",
    "Content-Length": content.length.toString(),
    "X-Custom-Header": "ProcesarEstoInmediatamente",
});

El contenido puede ser consultado y obtenido de la siguiente forma:

console.log(reqHeaders.has("Content-Type")); // true
console.log(reqHeaders.has("Set-Cookie")); // false
reqHeaders.set("Content-Type", "text/html");
reqHeaders.append("X-Custom-Header", "OtroValor");
 
console.log(reqHeaders.get("Content-Length")); // 11
console.log(reqHeaders.getAll("X-Custom-Header")); // ["ProcesarEstoInmediatamente", "OtroValor"]
 
reqHeaders.delete("X-Custom-Header");
console.log(reqHeaders.getAll("X-Custom-Header")); // []

Algunas de estas operaciones solamente son usadas en ServiceWorkers, pero proveen una mejor API a Headers.

Ya que las Headers puede ser enviadas en peticiones, o recibidas en respuestas, y tienen varias limitaciones sobre qué información pueda ser alterada, los objetos Headers tienen una propiedad llamada guard. Esta no está expuesta a la Web, pero afecta en qué operaciones de alteramiento están permitidas en el objeto Headers. Los posibles valores son:

  • “none”: valor por defecto.
  • “request”: vigila por un objeto Headers obtenido de un Request (Request.header).
  • “request-no-cors”: vigila por un objeto Headers obtenido de un Request creado con el modo “no-cors”.
  • “response”: naturalmente, por Headers obtenido de Response (Response.headers).
  • “immutable”: mayormente usado por ServiceWorkers, renderiza un objeto Headers de sólo lectura.

Los detalles de cada una de los vigilantes que afecta el comportamiento del objeto Headers están en la especificación. Por ejemplo, no puedes añadir o establecer una cabecera “Content-Length” de un Headers vigilado. Similarmente, insertar “Set-Cookie” dentro de una cabecera Response no está permitido así que los ServiceWorkers no pueden establecer cookies a través de Responses sintetizados.

Todos los métodos de Headers lanzan un TypeError si name no es un nombre de cabecera HTTP válido. Las operaciones de mutación lanzarán un TypeError si hay una vigilante inmutable. De otra forma fallan silenciosamente. Por ejemplo:

var res = Response.error();
try {
    res.headers.set("Origin", "http://mybank.com");
} catch(e) {
    console.log("No puedes pretender ser un banco!");
}

Request

La interfaz Request define una petición para obtener un recurso sobre HTTP. URL, método y cabeceras son esperados, pero Request también permite especificar un body, un modo de petición, credenciales e informes de caché.

El Request más sencillo es claro, sólo una URL, como cuando haces un GET a un recurso.

var req = new Request("/index.html");
console.log(req.method); // "GET"
console.log(req.url); // "http://example.com/index.html"

También puedes pasar un Request al constructor Request() para crear una copia (Esto no es lo mismo que llamar al método clone(), que es cubierto en la sección “Tratando con los cuerpos de las peticiones”).

var copy = new Request(req);
console.log(copy.method); // "GET"
console.log(copy.url); // "http://example.com/index.html"

De nuevo, esta forma solo es útil probablemente en ServiceWorkers.

Los atributos no URL de Request sólo pueden ser establecidos pasando valores iniciales como segundo argumento al constructor. Este argumento es un diccionario.

var uploadReq = new Request("/uploadImage", {
    method: "POST",
    headers: {
        "Content-Type": "image/png",
    },
    body: "image data"
});

El modo de Request es usado para determinar si las peticiones cross-origin provocan respuestas válidas, y qué propiedades en la respuesta son legibles. Los valores para el modo son “same-origin“, “no-cors” (por defecto) y “cors“.

El modo “same-origin” es simple, si una petición es hecha a otro origen con este modo establecido, el resultado es simplemente un error. Puedes usar este para asegurarte que una petición está siempre siendo hecha a tu origen.

var arbitraryUrl = document.getElementById("url-input").value;
fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) {
    console.log("Respuesta exitosa?", res.ok);
}, function(e) {
    console.log("Por favor, ingresa una URL del mismo origen!");
});

El modo “no-cors” captura lo que la plataforma web hace por defecto por scripts que importas desde algunos CDN, imágenes almacenadas en otros dominios, y así. Primero, impide el método de iniciar algo más que “HEAD”, “GET” o “POST”. Segundo, si cualquier ServiceWorker intercepta esas peticiones, no pueden agregar o sobrescribir cualquier cabecera excepto por estas. Tercero, JavaScript no puede acceder a cualquier propiedad de la Response resultante. Esto asegura que los ServiceWorkers no afectan a la semántica de la Web y previene problemas de seguridad y privacidad que podrían surgir de fugas de datos a través de dominios.

El modo “cors” el que usarás por lo general para hacer peticiones cross-origin conocidas para acceder a varias API ofrecidas por proveedores. Se espera que estas se adhieran al protocolo CORS. Solamente se espera un conjunto limitado de cabeceras en la Response, pero el cuerpo es legible. Por ejemplo, podrías obtener una lista de fotos más interesantes desde Flickr de hoy con algo como esto:

var u = new URLSearchParams();
u.append('method', 'flickr.interestingness.getList');
u.append('api_key', '<inserta el tu api key aquí>');
u.append('format', 'json');
u.append('nojsoncallback', '1');
 
var apiCall = fetch('https://api.flickr.com/services/rest?' + u);
 
apiCall.then(function(response) {
    return response.json().then(function(json) {
        // photo es una lista de fotos.
        return json.photos.photo;
    });
}).then(function(photos) {
    photos.forEach(function(photo) {
        console.log(photo.title);
    });
});

No puedes leer la cabecera “Date” ya que Flickr no lo permite a través de Access-Control-Expose-Headers.

response.headers.get("Date"); // null

La enumeración credentials determina si las cookies para el otro dominio son enviadas a peticiones cross-origin. Este es similar a la bandera withCredentials, pero con tres valores “omit” (default), “same-origin” e “include“.

El objeto Request también tendrá la habilidad de ofrecer informes de almacenamiento en caché al user-agent. Esto está actualmente en fase de revisión de seguridad. Firefox expone el atributo pero no tiene efecto.

Las peticiones tiene dos atributos de sólo lectura que son relevantes para los ServiceWorkers interceptándolos. Hay la cadena de texto referrer, que es establecida por el user-agent para ser el referente de Request. Esto puede ser una cadena vacía. La otra es context que es más que una gran enumeración definiendo el tipo de recurso que esta siendo obtenido. Esto puede ser un “image” si la petición es desde una etiqueta en el documento controlado, “worker” si es un intento de cargar un script worker, y así. Cuando es usado con la función fetch(), esta es “fetch”.

Response

Las instancias Response son retornadas por llamadas a fetch(). Estas también pueden ser creadas mediante JS, pero esto solo es útil en ServiceWorkers.

Ya hemos visto algunos atributos de Response cuando revisamos fetch(). Los candidatos más obvios son status, un valor entero (por defecto 200) y statusText (con valor por defecto “OK”), que corresponden al código de estado y razón de HTTP. El atributo ok es sólo una forma corta para verificar que status está en el rango 200-299.

headers es el objeto Headers de Response, que vigila “response”. El atributo url refleja la URL a la correspondiente petición.

Response también tiene un atributo type, que puede ser “basic”, “cors”, “default”, “error” u “opaque”.

  • basic“: Normal, respuesta del mismo origen. con todas las cabeceras expuestas excepto “Set-Cookie” y “Set-Cookie2”.
  • cors“: La respuesta fue recibida desde una petición cross-origin válida. Ciertas cabeceras y el cuerpo pueden ser accedidas.
  • error“: Error de red. Ninguna información útil que describe el error está disponible. El estado de Response es 0, las cabeceras están vacías e inmutables. Este es el tipo para una Respuesta obtenida desde Response.error().
  • opaque“: La respuesta para una petición “no-cors” a un recurso cross-origin. Muy restringido.

El tipo “error” resulta en la promesa de fetch() rechazándolo con TypeError.

Hay ciertos atributos que son útiles sólamente en un contexto ServiceWorker. La forma adecuada para devolver un Response a una petición interceptada en ServiceWorkers:

addEventListener('fetch', function(event) {
    event.respondWith(new Response("Response body", {
        headers: { "Content-Type" : "text/plain" }
    });
});

Como puedes ver, Response tiene dos argumentos en su constructor, en donde ambos son opcionales. El primer argumento es un cuerpo inicializador, y el segundo es un diccionario para establecer los status, statusText y headers.

El método estático Response.error() simplemente devuelve una respuesta de error. Similarmente Response.redirect(url, status) devuelve un Response que resulta en una redirección a url.

Tratando con los cuerpos de las peticiones

Ambas Request y Response pueden contener datos en su cuerpo. Hemos estado pasando por alto esto, porque son varios los tipos que un cuerpo pueden contener, pero los cubriremos en detalle, ahora.

Un cuerpo es una instancia de cualquiera de los siguientes tipos.

Además, Request y Response ofrecen los siguiente métodos para extraer el contenido en su cuerpo. Todos estos revuelven un Promise que generalmente se resuelve con el contenido actual.

  • arrayBuffer()
  • blob()
  • json()
  • text()
  • formData()

¡Esta es una mejora significante sobre XHR en términos de facilitar el uso de datos que no son texto!

El contenido del cuerpo de Request pueden ser establecidos pasando parámetros a body:

var form = new FormData(document.getElementById('login-form'));
fetch("/login", {
    method: "POST",
    body: form
});

Response toma el primer argumento como contenido de su cuerpo.

var res = new Response(
    new File(
        ["chunk", "chunk"],
        "archive.zip",
        { type: "application/zip" }
    )
);

Request y Response (y por extensión, la función fetch()), intentarán de una forma inteligente de determinar el tipo de contenido. Request también establecerá de manera automática una cabecera “Content-Type” si no es establecido en el diccionario de datos.

Transmisión y clonado

Es importante saber que los cuerpos de Request y Response pueden ser leídos sólo una vez. Ambas interfaces tiene un atributo booleano bodyUsed para determinar si es seguro de leer o no.

var res = new Response("one time use");
 
console.log(res.bodyUsed); // false
res.text().then(function(v) {
    console.log(res.bodyUsed); // true
});
 
console.log(res.bodyUsed); // true 
res.text().catch(function(e) {
    console.log("Intentado leer un Response terminado");
});

Esta decisión permite fácilmente la transición a una posible API Fetch basada en transmisión (streams). La intención es permitirle a las aplicaciones consumir datos que estén llegando, permitiendo a JavaScript tratar con grandes archivos como videos, y realizar cosas como la compresión y edición al vuelo.

Incluso, querrás acceder al cuerpo muchas veces. Por ejemplo, puedes usar la reciente API de Caché para almacenar Request y Response para uso sin conexión, ya que Caché requiere que los cuerpos estén disponibles para lectura.

Así que ¿cómo puedes leer los cuerpos múltiples veces con esto como restricción? La API provee un método clone() en las dos interfaces. Esto devolverá un clon del objeto, con un “nuevo” cuerpo. clone() DEBE ser llamada antes que el cuerpo del objeto correspondiente haya sido usado. Es decir, clone() primero, leer después.

addEventListener('fetch', function(evt) {
    var sheep = new Response("Dolly");
    console.log(sheep.bodyUsed); // false
    var clone = sheep.clone();
    console.log(clone.bodyUsed); // false
 
    clone.text();
    console.log(sheep.bodyUsed); // false
    console.log(clone.bodyUsed); // true
 
    evt.respondWith(cache.add(sheep.clone()).then(function(e) {
        return sheep;
    });
});

Mejoras futuras

Junto con la transición a transmisión, Fetch eventualmente tendrá la capacidad de abortar los fetch() que está ejecutándose y algunas formas de reportar el progreso de un fetch. Estas son proporcionadas por XHR, pero es un poco complicado para hacer encajar en la naturaleza basada en promesas de la API Fetch.

Tú también puedes contribuir a la evolución de esta API participando en las discusiones de la lista de correo de WHATWG y en los temas de las especificaciones Fetch y ServiceWorker.

Un agradecimiento a Andrea Marchesini, Anne van Kesteren y Ben Kelly por ayudar con la especificación e implementación.

The following two tabs change content below.

AngelFQC

Web Developer at BeezNest Latino
Ingeniero de Sistemas y Computación. Desarrollador PHP. Mozilla Peru. Chamilo LMS Developer.