miércoles, 23 de octubre de 2013

Patrón Post/Redirect/Get (PRG)

Este patrón indica una forma de diseñar una aplicación web para evitar el problema del doble envío de un formulario al recargar una página. Este problema también se conoce como doble POST y es la causa de que, a veces, al recargar una página el navegador muestre el siguiente mensaje:

Mostrar una pantalla como esta, ante una acción que el usuario considera normal, es un problema grave de usabilidad. Ademas de la presentación de este mensaje, el doble POST puede duplicar en el servidor una acción que ya se había realizado, como una compra, una trasferencia, el envío de un mensaje, etc.

Vamos a ver en detalle porqué se produce este problema y la solución que propone el patrón Post/Redirect/Get

El Problema

Vamos a suponer las siguientes acciones:

  • El usuario rellena un formulario para hacer una compra.
  • Pulsa el botón 'Enviar' para realizar la compra.
  • El servidor recibe la petición, hace el cargo en la cuenta del usuario y formaliza el pedido guardandolo en la base de datos.
  • Se muestra al usuario una página de 'Compra Realizada' con un resumen de los datos de de la compra.

Este proceso es muy habitual en las aplicaciones web. La secuencia puede darse en muchos casos: una página de compra, una página para enviar un mensaje a un foro, una página para dar de alta a un usuario, etc.

La comunicación entre cliente y servidor puede verse en la siguiente imagen (sacada de Wikipedia):

En general, cuando una petición va a modificar algo en el servidor se envía mediante un mensaje POST de HTTP, como vemos en el dibujo. El problema es que si el usuario decide recargar la página (quizá no se ha mostrado bien el resumen de la compra), el navegador tiene que volver a enviar la petición que devuelve esa página. Como esa petición es un POST, implica modificar algo en el servidor, en este caso realizar de nuevo la compra. Por eso los navegadores muestran el mensaje de aviso.

Naturalmente la aplicación en el servidor puede comprobar si esta petición ya se ha realizado y evitar la duplicación, pero esto no siempre se hace. El mensaje del navegador aparecería en cualquier caso, empeorando la experiencia de usuario.

Resumiendo: Una recarga de la página puede causar la duplicación de una acción en el servidor.

La Solución

La solución que propone el patrón Post/Redirect/Get es evitar que la página que se presenta al usuario despues de la acción sea el resultado directo del POST. Se devuelve un codigo de redirección que obliga al navegador a pedir la página mediante un nuevo GET. La secuencia sería la siguiente:

En este caso, despues del POST el servidor no envía la página de 'Compra Realizada', envía una respuesta indicando una redirección, mediante las siguientes cabeceras:

HTTP/1.1 303 See Other
Location: http://comprando.com/compraRealizada

Al recibir esta respuesta el navegador automáticamente hace una nueva petición (GET) a la dirección que se indica en cabecera Location. Como respuesta a este GET, el servidor devuelve la página de 'Compra Realizada'.

Ahora, si el usuario decide recargar la página, el navegador vuelve a enviar la petición GET y la carga de nuevo. Ya no se envía el POST original y no hay peligro de modificación de datos en el servidor.

Precisamente la redirección 303 se creó para este propósito. En la especificación HTTP 1.1 podemos leer:

303 "See Other" The response to the request can be found under a different URI and SHOULD be retrieved using a GET method on that resource. This method exists primarily to allow the output of a POST-activated script to redirect the user agent to a selected resource. The new URI is not a substitute reference for the originally requested resource. The 303 response MUST NOT be cached, but the response to the second (redirected) request might be cacheable.

¿Que pasa si pulsamos 'Back' en el navegador?

Cuando estamos en la página de 'Compra Realizada', si pulsamos 'atras' en el navegador, iríamos a la página original del formulario, que normalmente se leerá de la cache del navegador. Esta acción no envía de nuevo el formulario.

Si despues pulsamos la flecha "adelante" en el navegador iremos de nuevo a la página de resultado de la compra, pero esta acción no envía de nuevo el formulario.

Por supuesto, si cuando estamos en la página del formulario pulsamos el botón de 'Envíar' sí se enviará de nuevo. Este caso sólo puede protegerse en la parte del servidor. Podríamos por ejemplo, crear una cookie con información de la transacción y descartarla si llega de nuevo en una rango temporal de segundos.

miércoles, 16 de octubre de 2013

JavaScript: Obtener la fecha actual en milisegundos

Tarde o temprano necesitaremos obtener la fecha actual en algún punto de nuestro programa. Si tenemos que hacer operaciones con ella, lo habitual es manejarla internamente en milisegundos (UNIX timestamp) para luego transformarla en un formato legible al presentarla en pantalla.

Tiempo Unix es un sistema para la descripción de instantes de tiempo: se define como la cantidad de segundos transcurridos desde las 00:00:00 UTC del 1 de enero de 1970.

En javascript hay dos formas equivalentes de obtener la fecha actual en tiempo UNIX:

var now1 = new Date().getTime(); //slow

var now2 = Date.now()   //fast. ECMAScript 5. Not supported for IE<9

El segundo método es múcho más rápido que el primero. Vemos a continuación los detalles de cada uno.

Método 1: new Date().getTime()

La forma 'clásica' de obtener el timestamp actual es:

var now = new Date().getTime(); 

Lo que estamos haciendo en una sóla línea es obtener un objeto Date, que por defecto se crea con la fecha y hora actual, y despues llamar a su método .getTime() que nos devuelve la fecha en milisegundos UNIX time. El resultado es un número del tipo 1381852003756.

Es una forma resumida de hacer:

var tmpDate = new Date();     //  Wed Oct 16 2013 12:37:29 GMT+0200
var now = tmpDate.getTime();  //  1381919849147

Método 2: Date.now()

Este método fue estandarizado en ECMAScript 5.

var now = Date.now()   

Es más rápido y más intuitivo que el anterior. Es importante tener en cuenta que no está soportado en IE8 ni anteriores.

La razón por la que esta solución es más rápida que la anterior es que no tiene que instanciar el objeto Date, accedemos directamente al método.


'Shim' para utilizarlo en cualquier navegador

Si queremos utilizar la versión moderna pero tenemos que dar soporte a IE8 o anteriores, podemos crear fácilmente una función que emule Date.now() en los navegadores en los que no está disponible:

if (!Date.now) {
  Date.now = function now() {
    return new Date().getTime();
  };
}

Esta función simplemente comprueba si existe el método Date.now y si no existe lo crea emulandolo mediante new Date().getTime().