11.7. Manejo de redirecciones

Puede admitir redirecciones permanentes y temporales usando un tipo diferente de manipulador de URL.

Primero veamos por qué es necesario registrar los manipuladores.

Ejemplo 11.10. Acceso a servicios web sin admitir redirecciones

>>> import urllib2, httplib
>>> httplib.HTTPConnection.debuglevel = 1           1
>>> request = urllib2.Request(
...     'http://diveintomark.org/redir/example301.xml') 2
>>> opener = urllib2.build_opener()
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: '
GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 301 Moved Permanently\r\n'             3
header: Date: Thu, 15 Apr 2004 22:06:25 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml  4
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0                              5
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:06:25 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.url                                               6
'http://diveintomark.org/xml/atom.xml'
>>> f.headers.dict
{'content-length': '15955', 
'accept-ranges': 'bytes', 
'server': 'Apache/2.0.49 (Debian GNU/Linux)', 
'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 
'connection': 'close', 
'etag': '"e842a-3e53-55d97640"', 
'date': 'Thu, 15 Apr 2004 22:06:25 GMT', 
'content-type': 'application/atom+xml'}
>>> f.status
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: addinfourl instance has no attribute 'status'
1 Será capaz de ver mejor lo que sucede si activa la depuración.
2 Esta URL la he dispuesto con una redirección permanente a mi feed Atom en http://diveintomark.org/xml/atom.xml.
3 Exacto, cuando intenta descargar los datos de esa dirección, el servidor envía un código de estado 301 diciéndonos que se movió el recurso de forma permanente.
4 El servidor también responde con una cabecera Location: que indica el lugar de la nueva dirección de estos datos.
5 urllib2 lee el código de estado e intenta automáticamente descargar los datos de la nueva dirección especificada en la cabecera Location:.
6 El objeto que nos devuelve el opener contiene la nueva dirección permanente y todas la cabeceras de la segunda respuesta (obtenidas de la nueva dirección permanente). Pero no está el código de estado, así que no tenemos manera programática de saber si esta redirección era temporal o permanente. Y eso importa mucho: si es temporal entonces podemos continuar pidiendo los datos en el sitio antiguo. Pero si fuera una redirección permanente (como así es), deberíamos buscar los datos en el nuevo sitio desde ahora.

Esto no es óptimo, pero sí de fácil arreglo. urllib2 no se comporta exactamente como querríamos cuando se encuentra un 301 o un 302, así que tendremos que cambiar su comportamiento. ¿Cómo? Con un manipulador URL personalizado, exactamente igual que hicimos para trabajar con los códigos 304.

Ejemplo 11.11. Definición del manipulador de redirección

Esta clase está definida en openanything.py.


class SmartRedirectHandler(urllib2.HTTPRedirectHandler):     1
    def http_error_301(self, req, fp, code, msg, headers):  
        result = urllib2.HTTPRedirectHandler.http_error_301( 2
            self, req, fp, code, msg, headers)              
        result.status = code                                 3
        return result                                       

    def http_error_302(self, req, fp, code, msg, headers):   4
        result = urllib2.HTTPRedirectHandler.http_error_302(
            self, req, fp, code, msg, headers)              
        result.status = code                                
        return result                                       
1 El comportamiento de redirección lo define en urllib2 una clase llamada HTTPRedirectHandler. No queremos sustituirlo completamente, sólo extenderlo un poco, así que derivaremos HTTPRedirectHandler de manera que podamos llamar a la clase ancestro para que haga todo el trabajo duro.
2 Cuando encuentra un código de estado 301 del servidor, urllib2 buscará entre sus manipuladores y llamará al método http_error_301. Lo primero que hace el nuestro es llamar al método http_error_301 del ancestro, que hace todo el trabajo duro de buscar la cabecera Location: y seguir la redirección a la nueva dirección.
3 Aquí está lo importante: antes de volver almacenamos el código de estado (301), de manera que pueda acceder a él después el programa que hizo la llamada.
4 Las redirecciones temporales (código 302) funcionan de la misma manera: sustituimos el método http_error_302, llamamos al ancestro y guardamos el código de estado antes de volver.

¿Qué hemos conseguido con esto? Ahora podemos crear un abridor de URL con el manipulador personalizado de redirecciones, y seguirá accediendo automáticamente a la nueva dirección, pero ahora también podemos averiguar el código de estado de la redirección.

Ejemplo 11.12. Uso del manejador de redirección para detectar redirecciones permanentes

>>> request = urllib2.Request('http://diveintomark.org/redir/example301.xml')
>>> import openanything, httplib
>>> httplib.HTTPConnection.debuglevel = 1
>>> opener = urllib2.build_opener(
...     openanything.SmartRedirectHandler())           1
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: 'GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 301 Moved Permanently\r\n'            2
header: Date: Thu, 15 Apr 2004 22:13:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:13:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml

>>> f.status                                           3
301
>>> f.url
'http://diveintomark.org/xml/atom.xml'
1 Primero creamos un abridor de URL con el manejador de redirecciones que acabamos de definir.
2 Enviamos una petición y obtuvimos un código de estado 301 por respuesta. Llegados aquí, se invoca el método http_error_301. Llamamos al método ancestro, que sigue la redirección y envía una petición al nuevo sitio (http://diveintomark.org/xml/atom.xml).
3 Aquí está nuestra recompensa: ahora no sólo podemos acceder a la nueva URL, sino que también accedemos al código de estado de la redirección, de manera que podemos saber que fue una permanente. La próxima vez que pidamos estos datos deberíamos hacerlo a la dirección nueva (http://diveintomark.org/xml/atom.xml, como se especifica en f.url). Si hemos almacenado la dirección en un fichero de configuración o base de datos, deberemoos actualizarla de manera que no sigamos molestando al servidor con peticiones en la dirección antigua. Es hora de actualizar la libreta de direcciones.

El mismo manejador de redirecciones también puede decirle si no debe actualizar la libreta de direcciones.

Ejemplo 11.13. Uso del manejador de redirección para detectar redirecciones temporales

>>> request = urllib2.Request(
...     'http://diveintomark.org/redir/example302.xml')   1
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: '
GET /redir/example302.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 302 Found\r\n'                           2
header: Date: Thu, 15 Apr 2004 22:18:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 314
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0                                3
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:18:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.status                                              4
302
>>> f.url
http://diveintomark.org/xml/atom.xml
1 Ésta es una URL de ejemplo que he preparado configurada para decirle a los clientes que se redirijan temporalmente a http://diveintomark.org/xml/atom.xml.
2 El servidor devuelve un código de estado 302 que indica una redirección temporal. La nueva situación temporal de los datos la da la cabecera Location:.
3 urllib2 invoca nuestro método http_error_302, quien a su vez llama al método ancestro del mismo nombre en urllib2.HTTPRedirectHandler, que sigue la redirección al nuevo sitio. Entonces nuestro método http_error_302 almacena el código de estado (302) para que la aplicación que lo invocó pueda obtenerlo después.
4 Y aquí estamos, tras seguir con éxito la redirección a http://diveintomark.org/xml/atom.xml. f.status nos dice que fue una redirección temporal, lo que significa que deberíamos seguir pidiendo los datos a la dirección original (http://diveintomark.org/redir/example302.xml). Quizá nos redirija también la siguiente vez, o quizá no. Puede que nos redirija a un sitio diferente. No es decisión nuestra. El servidor dijo que esta redirección sólo es temporal, así que deberíamos respetarlo. Y ahora estamos exponiendo a la aplicación que invoca información suficiente para que respete eso.