¿Cómo funciona la carga de archivos HTTP?


Cuando envío un formulario simple como este con un archivo adjunto:

<form enctype="multipart/form-data" action="http://localhost:3000/upload?upload_progress_id=12344" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose a file to upload: <input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>

¿Cómo envía el archivo internamente? ¿Se envía el archivo como parte del cuerpo HTTP como datos? En los encabezados de esta solicitud, no veo nada relacionado con el nombre del archivo.

Solo me gustaría conocer el funcionamiento interno del HTTP al enviar un archivo.

Author: Mark Amery, 2011-12-28

5 answers

Echemos un vistazo a lo que sucede cuando seleccionas un archivo y envías tu formulario (He truncado los encabezados por brevedad):

POST /upload?upload_progress_id=12344 HTTP/1.1
Host: localhost:3000
Content-Length: 1325
Origin: http://localhost:3000
... other headers ...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="uploadedfile"; filename="hello.o"
Content-Type: application/x-object

... contents of file goes here ...
------WebKitFormBoundaryePkpFF7tjBAqx29L--

En lugar de codificar la URL de los parámetros del formulario, los parámetros del formulario (incluidos los datos del archivo) se envían como secciones en un documento de varias partes en el cuerpo de la solicitud.

En el ejemplo anterior, puede ver la entrada MAX_FILE_SIZE con el valor establecido en el formulario, así como una sección que contiene los datos del archivo. El nombre del archivo es parte de la Content-Disposition cabecera.

Los detalles completos están aquí.

 220
Author: toddsundsted,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-24 12:50:23

¿Cómo envía el archivo internamente?

El formato se llama multipart/form-data, como se preguntó en: ¿Qué significa enctype='multipart/form-data'?

Voy a:

  • añadir algunas referencias HTML5
  • explique por qué tiene razón con un formulario enviar ejemplo

Referencias HTML5

Hay tres posibilidades para enctype:

Cómo generar los ejemplos

Una vez que vea un ejemplo de cada método, se hace obvio cómo funcionan y cuándo debe usar cada uno.

Puede producir ejemplos usando:

Guarde el formulario en un archivo mínimo .html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>upload</title>
</head>
<body>
  <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
  <p><input type="text" name="text1" value="text default">
  <p><input type="text" name="text2" value="a&#x03C9;b">
  <p><input type="file" name="file1">
  <p><input type="file" name="file2">
  <p><input type="file" name="file3">
  <p><button type="submit">Submit</button>
</form>
</body>
</html>

Establecemos el valor de texto predeterminado en a&#x03C9;b, que significa aωb porque ω es U+03C9, que son los bytes 61 CF 89 62 en UTF-8.

Crear archivos para cargar:

echo 'Content of a.txt.' > a.txt

echo '<!DOCTYPE html><title>Content of a.html.</title>' > a.html

# Binary file containing 4 bytes: 'a', 1, 2 and 'b'.
printf 'a\xCF\x89b' > binary

Ejecute nuestro pequeño servidor echo: {[46]]}

while true; do printf '' | nc -l 8000 localhost; done

Abra el HTML en su navegador, seleccione los archivos y haga clic en enviar y comprobar el terminal.

nc imprime la solicitud recibida.

Probado en: Ubuntu 14.04.3, nc BSD 1.105, Firefox 40.

Multiparte / form-data

Firefox enviado:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150
Content-Length: 834

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text1"

text default
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text2"

aωb
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain

Content of a.txt.

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html

<!DOCTYPE html><title>Content of a.html.</title>

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream

aωb
-----------------------------735323031399963166993862150--

Para el archivo binario y el campo de texto, los bytes 61 CF 89 62 (aωb en UTF-8) se envían literalmente. Se podría verificar con nc -l localhost 8000 | hd, que dice que los bytes:

61 CF 89 62

Fueron enviados (61 == 'a' y 62 == "b").

Por lo Tanto, es claro que:

  • Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266 establece el tipo de contenido en multipart/form-data y dice que los campos están separados por la cadena boundary dada.

  • Cada campo recibe algunos encabezados secundarios antes de sus datos: Content-Disposition: form-data;, el campo name, el filename, seguido de los datos.

    El servidor lee los datos hasta la siguiente cadena de límite. El navegador debe elegir un límite que no aparecerá en ninguno de los campos, por lo que es por eso que el límite puede varíe entre las solicitudes.

    Debido a que tenemos el límite único, no es necesaria la codificación de los datos: los datos binarios se envían tal cual.

    TODO: ¿cuál es el tamaño de límite óptimo (log(N) Apuesto), y el nombre / tiempo de ejecución del algoritmo que lo encuentra? Consultado en: https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type se determina automáticamente por el navegador.

    Cómo se determina exactamente se preguntó en: ¿Cómo se determina el tipo mime de un archivo cargado por el navegador?

Application / x-www-form-urlencoded

Ahora cambie el enctype a application/x-www-form-urlencoded, vuelva a cargar el navegador y vuelva a enviar.

Firefox enviado:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

text1=text+default&text2=a%CF%89b&file1=a.txt&file2=a.html&file3=binary

Claramente no se enviaron los datos del archivo, solo los nombres de base. Así que esto no se puede usar para archivos.

En cuanto al campo de texto, vemos que los caracteres imprimibles habituales como a y b se enviaron en un byte, mientras que los no imprimibles como 0xCF y 0x89 tomaron 3 bytes cada uno: %CF%89!

Comparación

Las cargas de archivos a menudo contienen muchos caracteres no imprimibles (por ejemplo, imágenes), mientras que los formularios de texto casi nunca lo hacen.

De los ejemplos hemos visto que: {[46]]}

  • multipart/form-data: agrega unos pocos bytes de sobrecarga de límite al mensaje, y debe pasar algún tiempo calculándolo, pero envía cada byte en uno byte.

  • application/x-www-form-urlencoded: tiene un solo límite de bytes por campo (&), pero agrega un factor de sobrecarga lineal de 3x para cada carácter no imprimible.

Por lo tanto, incluso si pudiéramos enviar archivos con application/x-www-form-urlencoded, no querríamos, porque es tan ineficiente.

Pero para los caracteres imprimibles que se encuentran en los campos de texto, no importa y genera menos sobrecarga, por lo que solo lo usamos.

 181
Author: Ciro Santilli 新疆改造中心 六四事件 法轮功,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-21 15:48:15

Enviar archivo como contenido binario (subir sin formulario o FormData)

En las respuestas/ejemplos dados, el archivo es (muy probablemente) cargado con un formulario HTML o usando la API FormData. El archivo es solo una parte de los datos enviados en la solicitud, por lo multipart/form-data Content-Type cabecera.

Si desea enviar el archivo como único contenido, puede agregarlo directamente como el cuerpo de la solicitud y establecer el encabezado Content-Type al tipo MIME del archivo que está enviando. El nombre del archivo se puede agregar en el encabezado Content-Disposition. Puedes subirlo así:

var xmlHttpRequest = new XMLHttpRequest();

var file = ...file handle...
var fileName = ...file name...
var target = ...target...
var mimeType = ...mime type...

xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.send(file);

Si no (quiere) usar formularios y solo está interesado en cargar un solo archivo, esta es la forma más fácil de incluir su archivo en la solicitud.

 42
Author: Wilt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-11 08:00:35

Tengo este ejemplo de código Java:

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class TestClass {
    public static void main(String[] args) throws IOException {
        final ServerSocket socket = new ServerSocket(8081);
        final Socket accept = socket.accept();
        final InputStream inputStream = accept.getInputStream();
        final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        char readChar;
        while ((readChar = (char) inputStreamReader.read()) != -1) {
            System.out.print(readChar);
        }
        inputStream.close();
        accept.close();
        System.exit(1);
    }
}

Y tengo esta prueba.archivo html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File Upload!</title>
</head>
<body>
<form method="post" action="http://localhost:8081" enctype="multipart/form-data">
    <input type="file" name="file" id="file">
    <input type="submit">
</form>
</body>
</html>

Y finalmente el archivo que usaré para fines de prueba, llamado a. dat tiene el siguiente contenido:

0x39 0x69 0x65

Si interpreta los bytes anteriores como caracteres ASCII o UTF-8, en realidad representarán:

9ie

Así que vamos a ejecutar nuestro código Java, abrir prueba.html en nuestro navegador favorito, subir a.dat y enviar el formulario y ver lo que nuestro servidor recibe:

POST / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary06f6g54NVbSieT6y
DNT: 1
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.8,tr;q=0.6
Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF

------WebKitFormBoundary06f6g54NVbSieT6y
Content-Disposition: form-data; name="file"; filename="a.dat"
Content-Type: application/octet-stream

9ie
------WebKitFormBoundary06f6g54NVbSieT6y--

Bueno, no me sorprende ver los caracteres 9ie porque le dijimos a Java que los imprimiera tratándolos como caracteres UTF-8. También puede optar por leerlos como bytes raw..

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

Es en realidad el último encabezado HTTP aquí. Después de eso viene el Cuerpo HTTP, donde se puede ver el meta y el contenido del archivo que subimos.

 7
Author: Koray Tugay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-26 19:23:16

Un mensaje HTTP puede tener un cuerpo de datos enviado después de las líneas de encabezado. En una respuesta, aquí es donde el recurso solicitado se devuelve al cliente (el uso más común del cuerpo del mensaje), o tal vez texto explicativo si hay un error. En una solicitud, aquí es donde los datos introducidos por el usuario o los archivos cargados se envían al servidor.

Http://www.tutorialspoint.com/http/http_messages.htm

 5
Author: flagg19,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-28 18:42:26