11.6. Tratamiento de Last-Modified y ETag

Ahora que sabe cómo añadir cabeceras HTTP personalizadas a la consulta al servicio web, veamos cómo añadir la funcionalidad de las cabeceras Last-Modified y ETag.

Estos ejemplos muestran la salida con la depuración inactiva. Si sigue teniéndola activa tras la sección anterior puede desactivarla haciendo httplib.HTTPConnection.debuglevel = 0. O simplemente puede dejarla activa, si le apetece.

Ejemplo 11.6. Pruebas con Last-Modified

>>> import urllib2
>>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml')
>>> opener = urllib2.build_opener()
>>> firstdatastream = opener.open(request)
>>> firstdatastream.headers.dict                       1
{'date': 'Thu, 15 Apr 2004 20:42:41 GMT', 
 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 
 'content-type': 'application/atom+xml',
 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 
 'etag': '"e842a-3e53-55d97640"',
 'content-length': '15955', 
 'accept-ranges': 'bytes', 
 'connection': 'close'}
>>> request.add_header('If-Modified-Since',
...     firstdatastream.headers.get('Last-Modified'))  2
>>> seconddatastream = opener.open(request)            3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "c:\python23\lib\urllib2.py", line 326, in open
    '_open', req)
  File "c:\python23\lib\urllib2.py", line 306, in _call_chain
    result = func(*args)
  File "c:\python23\lib\urllib2.py", line 901, in http_open
    return self.do_open(httplib.HTTP, req)
  File "c:\python23\lib\urllib2.py", line 895, in do_open
    return self.parent.error('http', req, fp, code, msg, hdrs)
  File "c:\python23\lib\urllib2.py", line 352, in error
    return self._call_chain(*args)
  File "c:\python23\lib\urllib2.py", line 306, in _call_chain
    result = func(*args)
  File "c:\python23\lib\urllib2.py", line 412, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 304: Not Modified
1 ¿Recuerda todas aquellas cabeceras HTTP que vio impresas cuando activó la depuración? Así es como puede obtener acceso de forma programática a ellas: firstdatastream.headers es un objeto que actúa como un diccionario y le permite obtener cualquiera de las cabeceras individuales devueltas por el servidor HTTP.
2 En la segunda consulta añadimos la cabecera If-Modified-Since con la última fecha de modificación que nos dio la primera petición. Si los datos no han cambiado, el servidor debería devolver un código de estado 304.
3 Pues bien, los datos no han cambiado. Puede ver por la traza de depuración que urllib2 lanza una excepción especial, HTTPError, en respuesta del código de estado 304. Esto es un poco inusual y no ayuda demasiado. Después de todo no es un error; le indicamos específicamente al servidor que no nos enviase datos si no habían cambiado, y los datos no cambiaron así que el servidor nos dijo que no nos iba a enviar datos. Eso no es un error; es exactamente lo que esperábamos.

urllib2 también lanza una excepción HTTPError por condiciones en las que sí pensaríamos como errores, tales como 404 (página no encontrada). De hecho, lanzará HTTPError por cualquier código de estado diferente a 200 (OK), 301 (redirección permanente), o 302 (redirección temporal). Sería más útil a nuestros propósitos capturar el código de estado y devolverlo, simplemente, sin lanzar excepción. Para hacerlo necesitaremos definir un manipulador de URL personalizado.

Ejemplo 11.7. Definición de manipuladores de URL

Este manipulador de URL es parte de openanything.py.


class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):    1
    def http_error_default(self, req, fp, code, msg, headers): 2
        result = urllib2.HTTPError(                           
            req.get_full_url(), code, msg, headers, fp)       
        result.status = code                                   3
        return result                                         
1 urllib2 está diseñada sobre manipuladores de URL. Cada manipulador es una clase que puede definir cualquier cantidad de métodos. Cuando sucede algo (como un error de HTTP o incluso un código 304) urllib2 hace introspección en la lista de manipuladores definidos a la busca de un método que lo maneje. Usamos una introspección similar en Capítulo 9, Procesamiento de XML para definir manipuladores para diferentes tipos de nodo, pero urllib2 es más flexible e inspecciona todos los manipuladores que se hayan definido para la consulta actual.
2 urllib2 busca entre los manipuladores definidos y llama a http_error_default cuando encuentra que el servidor le envía un código de estado 304. Definiendo un manipulador personalizado podemos evitar que urllib2 lance una excepción. En su lugar, creamos el objeto HTTPError pero lo devolvemos en lugar de lanzar una excepción con él.
3 Ésta es la parte clave: antes de volver se ha de guardar el código de estado que devolvió el servidor HTTP. Esto nos permite acceder fácilmente a él desde el programa que llama.

Ejemplo 11.8. Uso de manipuladores URL personalizados

>>> request.headers                           1
{'If-modified-since': 'Thu, 15 Apr 2004 19:45:21 GMT'}
>>> import openanything
>>> opener = urllib2.build_opener(
...     openanything.DefaultErrorHandler())   2
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status                   3
304
>>> seconddatastream.read()                   4
''
1 Continuamos con el ejemplo anterior, así que aún existe el objeto Request y hemos añadido la cabecera If-Modified-Since.
2 Ésta es la clave: ahora que hemos definido nuestro manipulador URL personal, debemos decirle a urllib2 que lo use. ¿Recuerda cuando dije que urllib2 dividía el proceso de acceder a un recurso HTTP en tres pasos, y por buenas razones? Aquí tiene por qué la construcción del abridor de URL tiene su propio paso: porque puede hacerlo con su propio manipulador de URL personalizado que sustituya el comportamiento por omisión de urllib2.
3 Ahora podemos abrir el recurso tranquilamente, y lo que obtenemos junto a las cabeceras normales (use seconddatastream.headers.dict para acceder a ellas) es un objeto que contiene el código de estado de HTTP. En este caso, como esperábamos, el estado 304, lo que significa que estos datos no han cambiado desde la última vez que los solicitó.
4 Observe que cuando el servidor envía un código 304 no envía los datos. Ésa es la idea: ahorrar ancho de banda al no descargar de nuevo los datos que no hayan cambiado. Así que si realmente quiere los datos, necesitará guardarlos en una caché local la primera vez que los obtiene.

El tratamiento de ETag funciona de la misma manera, pero en lugar de buscar Last-Modified y enviar If-Modified-Since, buscamos ETag y enviamos If-None-Match. Empecemos una sesión nueva del IDE.

Ejemplo 11.9. Soporte de ETag/If-None-Match

>>> import urllib2, openanything
>>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml')
>>> opener = urllib2.build_opener(
...     openanything.DefaultErrorHandler())
>>> firstdatastream = opener.open(request)
>>> firstdatastream.headers.get('ETag')        1
'"e842a-3e53-55d97640"'
>>> firstdata = firstdatastream.read()
>>> print firstdata                            2
<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
  xmlns="http://purl.org/atom/ns#"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xml:lang="en">
  <title mode="escaped">dive into mark</title>
  <link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
  <-- rest of feed omitted for brevity -->
>>> request.add_header('If-None-Match',
...     firstdatastream.headers.get('ETag'))   3
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status                    4
304
>>> seconddatastream.read()                    5
''
1 Podemos obtener la ETag devuelta por el servidor usando el pseudo-diccionario firstdatastream.headers. (¿Qué sucede si el servidor no envió una ETag? Que esta línea devolvería None.)
2 Bien, tenemos los datos.
3 Ahora haga la segunda llamada indicando en la cabecera If-None-Match la ETag que obtuvo de la primera llamada.
4 La segunda llamada tiene un éxito sin aspavientos (no lanza una excepción), y de nuevo podemos ver que el servidor envió un código de estado 304. Sabe que los datos no han cambiado basándose en la ETag que enviamos la segunda vez.
5 Independientemente de si el 304 sucede debido a una comprobación de fecha Last-Modified o por comparación de una suma ETag, no obtendremos los datos junto con el 304. Y eso es lo que se pretendía.
nota
En estos ejemplos el servidor HTTP ha respondido tanto a la cabecera Last-Modified como a ETag, pero no todos los servidores lo hacen. Como cliente de un servicio web debería estar preparado para usar ambos, pero debe programar de forma defensiva por si se da el caso de que el servidor sólo trabaje con uno de ellos, o con ninguno.