El detalle maligno de WebClient

Que el diablo está en los detalles es algo que, tarde o temprano, terminas aprendiendo por las buenas o por las malas. Por suerte esta vez traigo un caso de los primeros, donde, tirando un poco del hilo, pude llegar a buen puerto sin arrancarme un solo pelo de desesperación. Os pongo en situación: un día cualquiera resulta que tienes a un grupo de usuarios que pueden seguir operando sin problemas cierto software que se conecta a un servicio online a través de una API REST, y otro grupo de usuarios que, utilizando la misma versión del software, no pueden tan siquiera autenticarse.

Piensas que, a lo mejor, ha cambiado algún endpoint en la API por el motivo que sea para este grupo (son usuarios nuevos, el servicio les ha dado una dirección diferente para introducir en la configuración…), y que a ello se deben los problemas de conexión. Extraño, pero entra en el abanico de posibilidades. Así que, antes de dar vueltas como un loco, intentas reproducir el problema. Te creas una cuenta de pruebas, enchufas el depurador, y a ver qué sucede.

Y resulta que tú tampoco te puedes autenticar contra la API. Y menos mal, porque un fallo que no se puede reproducir es un fallo que no se puede arreglar (al menos no sin dar palos de ciego). Así que miras el mensaje de la excepción, a ver si estás recibiendo un código Not Found, un Forbidden, o un Jarl Que No Puedorl. Y resulta que ni tan siquiera estás llegando a realizar alguna petición al servidor, el código falla incluso antes con un mensaje de de error que se puede interpretar, al igual que en cierto dicho popular español, como un perfecto “¿a dónde vas? – manzanas traigo” (cuando la respuesta que da alguien tiene poco o nada que ver con la pregunta formulada).

System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 245 characters.

¿Cómo puede ser que utilizando WebClient, que en ningún momento se espera que acceda al sistema de ficheros (sino, tal y como su propio nombre indica, a alguna parte de la Web), salte una excepción propia de la clase System.IO.Path? ¿Cómo puede ser que al intentar acceder a una URL perfectamente bien formada, con una longitud razonable, con la que POSTMAN no tiene inconveniente alguno en lidiar, provoque un error proveniente de alguna parte de Path diciendo que el nombre de fichero especificado es demasiado largo?  ¿Qué clase de perversidad está ocurriendo a nuestras espaldas para que se estén mezclando churras con merinas sin ningún tipo de pudor? Algo huele mal.

Exactamente mi cara al leer el mensaje de la excepción

Exactamente mi cara al leer el mensaje de la excepción

Los que estén de acuerdo con Jorge Manrique pueden ir predicando por ahí eso de que cualquier tiempo pasado fue mejor. A ellos les diría que busquen un problema en un componente de .NET Framework por métodos exóticos tales como señales de humo, tam-tam, o ouija (o desensamblado, pero eso ya son -supongo- terrenos farragosos). Pero por suerte, desde que Microsoft está en su época de aperturismo salvaje, el código fuente de .NET Framework se puede encontrar online en su mayor parte. Así que examinemos qué hace WebClient al ejecutar su método DownloadString(address : string):

Obtiene la Uri del string pasado como parámetro address y llama a la sobrecarga del método DownloadString que recibe como parámetro dicha Uri. Fácil, sencillo y para toda la familia. Continuemos pues con el método GetUri(path : string).

No nos saltemos en la lectura del método la documentación, así después podremos gritar a los cuatro vientos “¡Mentiras, sucias mentiras!”:

Parses the string uri into a properly formed uri – uses Uri class

“Uses Uri class”. Ahora leamos el cuerpo del método. Efectivamente, utiliza la clase Uri. Pero… vaya, también utiliza la clase Path para formar direcciones Web, podrían haberlo puesto también en la documentación.

Ahora ya podemos situar en el espacio-tiempo la excepción que saltaba antes incluso de poder conectar al servidor. “The specified path, file name, or both are too long”. Vean ustedes; el límite “de facto” para la longitud de una dirección Web es de unos 2000 caracteres, sentado, cómo no, por Internet Explorer. Versiones modernas (Microsoft Edge) admiten en su barra de direcciones, de hecho, 2048 caracteres como máximo. Ley escrita sobre la longitud máxima de una URI no hay ninguna, tal y como aclara el RFC 2616:

The HTTP protocol does not place any a priori limit on the length of a URI. Servers MUST be able to handle the URI of any resource they serve, and SHOULD be able to handle URIs of unbounded length if they provide GET-based forms that could generate such URIs. A server SHOULD return 414 (Request-URI Too Long) status if a URI is longer than the server can handle (see section 10.4.15).

Así que una clase preparada para descargar contenido de un servidor remoto, tal y como podrían imaginar ustedes en sus sueños más recatados y sensatos, debería estar preparada par el caso de recibir, como mínimo, una dirección de unos 2000 caracteres de longitud. Máxime viniendo de Microsoft y su límite de 2048 caracteres impuesto al usuario incluso en la barra de direcciones. Si nos ponemos a dejar volar la imaginación sin control, ya en nuestros sueños más salvajes, WebClient podría incluso recibir una dirección de una longitud prácticamente ilimitada, quedando, según el RFC 2616, en manos del servidor la responsabilidad de manejar tal eventualidad.

Pero el límite para un Path en Windows es de 260 caracteres EN TOTAL (esto es, incluyendo la unidad, los dos puntos de separación de unidad, y el caracter nulo de terminación), tal y como se puede leer en la documentación online al respecto.

In the Windows API (with some exceptions discussed in the following paragraphs), the maximum length for a path is MAX_PATH, which is defined as 260 characters. A local path is structured in the following order: drive letter, colon, backslash, name components separated by backslashes, and a terminating null character. For example, the maximum path on drive D is “D:\some 256-character path string<NUL>” where “<NUL>” represents the invisible terminating null character for the current system codepage. (The characters < > are used here for visual clarity and cannot be part of a valid path string.)

Esto es un poco contra-intuitivo. ¿Por qué 260 caracteres y no 256, para usar exactamente 32 bytes por path? Bueno, en realidad sí son 256 bytes, ya que la unidad no forma parte, estrictamente, de dicho path. Como curiosidad, la limitación en la longitud del Path ha desaparecido en Windows 10 (tan solo han tenido que pasar casi dos décadas), pero como esperan que quieras que el contenido de tus discos sea compatible con versiones antiguas de Windows (llamémosles precavidos), esta es una opt-in feature, teniendo que modificar a mano el registro para habilitarla.

Así pues, ¿cuál es el detalle maligno de WebClient? Ni más ni menos que utilizar la clase Path para formar una Uri, cuando Path fuerza la limitación de 260 caracteres propia de Windows desde los tiempos de 640K is enough RAM. ¿Cuál es la alternativa, reemplazar por completo WebClient en nuestras aplicaciones? No, por suerte esto es algo que se puede sortear. Para ello, en vez de utilizar DownloadString(address : string), lo que hay que tratar de hacer es conseguir formar la Uri antes, pasando a utilizar DownloadString(address : Uri), de forma que se esquivan todas las llamadas posibles a Path.

¡Saludos!

Deja un comentario