¿Por qué no se envían correctamente los nombres POST con Unicode cuando se usa multipart / form-data?

Quiero enviar una solicitud POST con un archivo adjunto, aunque algunos de los nombres de campo tienen caracteres Unicode en ellos. Pero no son recibidas correctamente por el servidor, como se ve a continuación:

>>> # normal, without unicode >>> resp = requests.post('http://httpbin.org/post', data={'snowman': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form'] >>> resp {u'snowman': u'hello'} >>> >>> # with unicode, see that the name has become 'null' >>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form'] >>> resp {u'null': u'hello'} >>> >>> # it works without the image >>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}).json()['form'] >>> resp {u'\u2603': u'hello'} 

¿Cómo puedo solucionar este problema?

De los comentarios de wireshark, parece que las peticiones de python lo están haciendo mal, pero puede que no haya una “respuesta correcta”.

RFC 2388 dice

Los nombres de campo originalmente en conjuntos de caracteres no ASCII pueden codificarse dentro del valor del parámetro “nombre” usando el método estándar descrito en RFC 2047.

RFC 2047 , a su vez, dice

En general, una “palabra codificada” es una secuencia de caracteres ASCII imprimibles que comienza con “=?”, Termina con “? =” Y tiene dos “?” En el medio. Especifica un conjunto de caracteres y un método de encoding, y también incluye el texto original codificado como caracteres ASCII gráficos, de acuerdo con las reglas para ese método de encoding.

y continúa describiendo los métodos de encoding “Q” y “B”. Usando el método “Q” (entre comillas y para imprimir), el nombre sería:

 =?utf-8?q?=E2=98=83?= 

PERO , como RFC 6266 establece claramente:

Una ‘palabra codificada’ NO DEBE usarse en el parámetro de un campo de tipo de contenido o disposición de contenido MIME, o en cualquier cuerpo de campo estructurado, excepto dentro de un ‘comentario’ o ‘frase’.

así que no se nos permite hacer eso. (Felicitaciones a @Lukasa por esta captura!)

RFC 2388 también dice

El nombre del archivo local original también se puede proporcionar, ya sea como un parámetro de “nombre de archivo” del encabezado “content-disposition: form-data” o, en el caso de varios archivos, en un encabezado “content-disposition: file” de la subparte. La aplicación de envío PUEDE proporcionar un nombre de archivo; Si el nombre del archivo del sistema operativo del remitente no está en US-ASCII, el nombre del archivo podría aproximarse o codificarse utilizando el método de RFC 2231.

Y RFC 2231 describe un método que se parece más a lo que estás viendo. En eso,

Los asteriscos (“*”) se reutilizan para proporcionar el indicador de que la información del juego de caracteres y el idioma está presente y se está utilizando la encoding. Se utiliza una comilla simple (“‘”) para delimitar el conjunto de caracteres y la información de idioma al comienzo del valor del parámetro. Los signos de porcentaje (“%”) se utilizan como indicador de encoding, que concuerda con el RFC 2047.

Específicamente, un asterisco al final de un nombre de parámetro actúa como un indicador de que el conjunto de caracteres y la información de idioma pueden aparecer al comienzo del valor del parámetro. Se utiliza una comilla simple para separar el conjunto de caracteres, el idioma y la información del valor real en la cadena de valor del parámetro, y un signo de porcentaje se usa para marcar los octetos codificados en hexadecimal.

Es decir, si este método se emplea (y se admite en ambos extremos), el nombre debe ser:

 name*=utf-8''%E2%98%83 

Afortunadamente, RFC 5987 agrega una encoding basada en RFC 2231 a los encabezados HTTP. (Felicitaciones a @bobince por este hallazgo). Dice que puede (de cualquier modo debería) incluir tanto un valor de estilo RFC 2231 como un valor simple:

Las especificaciones de campo de encabezado deben definir si se permiten varias instancias de parámetros con componentes de nombre de archivo idénticos y cómo deben procesarse. Esta especificación sugiere que un parámetro que usa la syntax extendida tiene prioridad. Esto permitiría a los productores utilizar ambos formatos sin romper destinatarios que aún no comprenden la syntax extendida.

Ejemplo:

foo: bar; título = “tipos de cambio de la EURO”; title * = utf-8 ”% e2% 82% ac% 20exchange% 20rates

Sin embargo, en su ejemplo, “reducen” el valor simple de “clientes heredados”. Esta no es realmente una opción para un nombre de campo de formulario, por lo que parece que el mejor enfoque podría ser incluir las versiones name= y name*= , donde el valor simple es (como lo describe @bobince) “simplemente enviando el bytes, citados, en la misma encoding que la forma “, como:

 Content-Disposition: form-data; name="☃"; name*=utf-8''%E2%98%83 

Ver también:

  • Encabezados HTTP de encoding / deencoding en Java
  • ¿Cómo puedo codificar un nombre de archivo de acuerdo con RFC 2231?
  • ¿Cómo codificar el parámetro de nombre de archivo del encabezado Content-Disposition en HTTP?

Finalmente, consulte http://larry.masinter.net/1307multipart-form-data.pdf (también https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909#c8 ), donde está se recomienda evitar el problema al seguir con los nombres de campo de formulario ASCII.

El valor del campo aparece como datos de formulario; nombre * = utf-8 ”% 5Cu2603 en Wireshark

Dos cosas aquí.

  1. Para mi no, recibo el name*=utf-8''%E2%98%83 . %5Cu2603 es lo que esperaría de escribir accidentalmente un escape \u en una cadena que no sea Unicode, es decir, escribir '\u2603' lugar de '☃' como se '☃' anteriormente.

  2. Como se comentó con cierta amplitud, esta es la forma RFC 2231 de encabezados Unicode extendidos:

El formato RFC 2231 anteriormente no era válido en HTTP (HTTP no es un estándar de correo en la familia RFC 822). Ahora ha sido llevado a HTTP por RFC 5987, pero como eso es bastante reciente, casi nada en el lado del servidor lo admite.

Definitivamente, urllib3 no debería confiar en ello; debería hacer lo que hacen los navegadores y simplemente enviar los bytes, citados, en la misma encoding que el formulario. Si debe usar el formulario 2231, debe estar en combinación, como en la sección 4.2 . por ejemplo, en urllib3.fields.format_header_param , en lugar de:

 value = email.utils.encode_rfc2231(value, 'utf-8') 

Tu puedes decir:

 value = '%s="%s"; %s*=%s' % ( name, value, name, email.utils.encode_rfc2231(value, 'utf-8') ) 

Sin embargo, incluso el formulario 2231 puede confundir algunos servidores antiguos.

Creo que soy el único culpable por el hecho de que urllib3 y, por lo tanto, las solicitudes producen el formato que hace. Cuando escribí ese código , tenía principalmente en mente los nombres de archivo, y la sección 4.5 del RFC 2388 sugiere el uso de ese formato RFC 2231 allí.

Con respecto a los nombres de campo, la sección 3 de RFC 2388 se refiere a RFC 2047 , que a su vez prohíbe el uso de palabras codificadas en Content-Disposition campos de Content-Disposition . Así que a mí y a otros me parece que estos dos estándares se contradicen entre sí. Pero tal vez el RFC 2338 debería tener prioridad , por lo que quizás el uso de palabras codificadas RFC 2047 sería más correcto.

Recientemente me han informado del hecho de que el borrador actual para el estándar HTML 5 tiene una sección sobre la encoding de datos de múltiples partes / formularios . Contradice varias otras normas, pero sin embargo podría ser el futuro. Con respecto a los nombres de campo (no los nombres de archivo), describe una encoding que convierte los caracteres en entidades XML decimales, por ejemplo, para tu muñeco de nieve. Sin embargo, esa encoding solo debe aplicarse si la encoding establecida para el envío no contiene el carácter en cuestión, lo que no debería ser el caso en su configuración.

He presentado un problema para urllib3 para discutir las consecuencias de esto y, probablemente, abordarlos en la implementación.

La respuesta de Rob Starling es muy perspicaz y demuestra que el uso de caracteres no ASCII en los nombres de los campos es una mala idea en cuanto a compatibilidad (¡todos esos RFC!), Pero logré que las solicitudes de python se adhieran a las más utilizadas (de lo que puedo ver) método de manejo de las cosas.

Dentro de site-packages/requests/packages/urllib3/fields.py , elimine esto (línea ~ 50):

 value = email.utils.encode_rfc2231(value, 'utf-8') 

Y cambia la línea justo debajo de esto a esto:

 value = '%s="%s"' % (name, value.decode('utf-8')) 

Esto hace que los servidores (que he probado) tomen el campo y lo procesen correctamente.