application / x-www-form-urlencoded czy multipart / form-data?

1335

W HTTP istnieją dwa sposoby wysyłania danych POST: application/x-www-form-urlencodedi multipart/form-data. Rozumiem, że większość przeglądarek może przesyłać pliki tylko wtedy, gdy multipart/form-datajest używana. Czy są jakieś dodatkowe wskazówki, kiedy używać jednego z typów kodowania w kontekście interfejsu API (bez przeglądarki)? Może to być na przykład oparte na:

  • rozmiar danych
  • istnienie znaków spoza ASCII
  • istnienie na (niekodowanych) danych binarnych
  • potrzeba przesłania dodatkowych danych (takich jak nazwa pliku)

Zasadniczo nie znalazłem w Internecie żadnych formalnych wskazówek dotyczących korzystania z różnych typów treści.

max
źródło
74
Należy wspomnieć, że są to dwa typy MIME, których używają formularze HTML. Sam HTTP nie ma takich ograniczeń ... można użyć dowolnego typu MIME, jakiego chce, za pośrednictwem HTTP.
tybro0103

Odpowiedzi:

2013

TL; DR

Podsumowanie; jeśli masz dane binarne (nie alfanumeryczne) (lub ładunek o znacznej wielkości) do przesłania, użyj multipart/form-data. W przeciwnym razie użyj application/x-www-form-urlencoded.


Wspomniane typy MIME to dwa Content-Typenagłówki żądań HTTP POST, które muszą obsługiwać programy klienckie (przeglądarki). Celem obu tych typów żądań jest przesłanie listy par nazwa / wartość na serwer. W zależności od rodzaju i ilości przesyłanych danych jedna z metod będzie wydajniejsza od drugiej. Aby zrozumieć, dlaczego, musisz spojrzeć na to, co każdy robi pod przykryciem.

Ponieważ application/x-www-form-urlencodedtreść wiadomości HTTP wysyłanej do serwera to zasadniczo jeden gigantyczny ciąg zapytania - pary nazwa / wartość są oddzielone znakiem ampersand ( &), a nazwy są oddzielone od wartości symbolem równości ( =). Przykładem tego może być: 

MyVariableOne=ValueOne&MyVariableTwo=ValueTwo

Zgodnie ze specyfikacją :

[Zastrzeżone i] znaki niealfanumeryczne są zastępowane przez „% HH”, znak procentu i dwie cyfry szesnastkowe reprezentujące kod ASCII znaku

Oznacza to, że dla każdego niealfanumerycznego bajtu, który istnieje w jednej z naszych wartości, jego reprezentacja zajmie trzy bajty. W przypadku dużych plików binarnych potrojenie ładunku będzie wysoce nieefektywne.

Właśnie tam multipart/form-data. W tej metodzie przesyłania par nazwa / wartość każda para jest reprezentowana jako „część” komunikatu MIME (jak opisano w innych odpowiedziach). Części są oddzielone określoną granicą łańcucha (wybraną specjalnie, aby ten łańcuch granicy nie występował w żadnym z ładunków „wartościowych”). Każda część ma swój własny zestaw nagłówków MIME, takich jak Content-Type, a zwłaszcza Content-Disposition, które mogą nadać każdej części jej „nazwę”. Element wartości każdej pary nazwa / wartość stanowi ładunek każdej części komunikatu MIME. Specyfikacja MIME daje nam więcej opcji przy reprezentowaniu ładunku wartości - możemy wybrać bardziej wydajne kodowanie danych binarnych w celu zaoszczędzenia przepustowości (np. Base 64 lub nawet surowe pliki binarne).

Dlaczego nie korzystać multipart/form-datacały czas? W przypadku krótkich wartości alfanumerycznych (jak większość formularzy internetowych) narzut związany z dodaniem wszystkich nagłówków MIME znacznie przewyższy wszelkie oszczędności wynikające z bardziej wydajnego kodowania binarnego.

Matt Bridges
źródło
84
Czy x-www-form-urlencoded ma limit długości, czy jest nieograniczony?
Pacerier
34
@Pacerier Limit jest egzekwowany przez serwer odbierający żądanie POST. Zobacz ten wątek, aby uzyskać więcej dyskusji: stackoverflow.com/questions/2364840/...
Matt Bridges
5
@ZiggyTheHamster JSON i BSON są bardziej wydajne dla różnych typów danych. Base64 jest gorszy od gzip, dla obu metod serializacji. Base64 nie przynosi żadnych korzyści, HTTP obsługuje binarne pyloady.
Tiberiu-Ionuț Stan
16
Zauważ też, że jeśli formularz zawiera przesyłanie nazwanego pliku, jedynym wyborem są dane w formularzu, ponieważ urlencoded nie ma sposobu na umieszczenie nazwy pliku (w danych formularza jest to parametr nazwy do dyspozycji).
Guido van Rossum
4
@EML zobacz mój w nawiasach ”(wybrany specjalnie, aby ten ciąg graniczny nie występował w żadnej z„ wartości ”ładunków)”
Matt Bridges
151

CZYTAJ CO NAJMNIEJ PIERWSZĄ PARĘ TUTAJ!

Wiem, że to 3 lata za późno, ale odpowiedź Matta (zaakceptowana) jest niekompletna i ostatecznie wpakuje cię w kłopoty. Kluczem tutaj jest to, że jeśli zdecydujesz się użyć multipart/form-data, granica nie może pojawić się w danych pliku, które serwer ostatecznie otrzyma.

Nie stanowi to problemu application/x-www-form-urlencoded, ponieważ nie ma granicy. x-www-form-urlencodedmoże również zawsze obsługiwać dane binarne, po prostu zamieniając jeden dowolny bajt na trzy 7BITbajty. Nieefektywne, ale działa (i zauważ, że komentarz o niemożności wysyłania nazw plików oraz danych binarnych jest niepoprawny; po prostu wysyłasz go jako inną parę klucz / wartość).

Problem multipart/form-datapolega na tym, że separator granic nie może być obecny w danych pliku (patrz RFC 2388 ; sekcja 5.2 zawiera również dość kiepską wymówkę, że nie ma odpowiedniego zagregowanego typu MIME, który pozwala uniknąć tego problemu).

Tak, na pierwszy rzut oka, multipart/form-datanie ma żadnej wartości w ogóle w jakikolwiek przesłanie pliku, binarny lub inaczej. Jeśli nie wybierzesz poprawnie granicy, w końcu będziesz mieć problem, niezależnie od tego, czy wysyłasz zwykły tekst, czy surowy plik binarny - serwer znajdzie granicę w niewłaściwym miejscu, a plik zostanie obcięty lub test POST zawiedzie.

Kluczem jest wybranie kodowania i granicy, tak aby wybrane znaki graniczne nie mogły pojawić się w zakodowanym wyjściu. Jednym prostym rozwiązaniem jest użycie base64( nie używaj surowego pliku binarnego). W base64 3 dowolne bajty są zakodowane w czterech 7-bitowych znakach, gdzie wyjściowy zestaw znaków to [A-Za-z0-9+/=](tj. Alfanumeryczny, „+”, „/” lub „=”). =jest szczególnym przypadkiem i może pojawić się tylko na końcu zakodowanego wyjścia jako pojedynczy =lub podwójny ==. Teraz wybierz swoją granicę jako 7-bitowy ciąg ASCII, który nie może pojawić się na base64wyjściu. Wiele wyborów, które widzisz w sieci, nie spełnia tego testu - MDN tworzy dokumenty, na przykład użyj „obiektu blob” jako granicy podczas wysyłania danych binarnych - źle. Jednak coś takiego jak „! Blob!” nigdy nie pojawi się na base64wyjściu.

EML
źródło
52
Podczas gdy rozważanie danych wieloczęściowych / formularzy jest zapewnieniem, że granica nie pojawia się w danych, jest to dość proste do osiągnięcia poprzez wybranie granicy, która jest wystarczająco długa. Aby tego dokonać, nie używaj kodowania base64. Granica który jest generowany losowo i takiej samej długości jak UUID powinny być wystarczające, aby: stackoverflow.com/questions/1705008/... .
Joshcodes
20
@EML, To w ogóle nie ma sensu. Oczywiście granica jest wybierana automatycznie przez klienta http (przeglądarkę), a klient będzie wystarczająco inteligentny, aby nie używać granicy, która koliduje z zawartością przesłanych plików. Jest to tak proste dopasowanie substringowe index === -1.
Pacerier,
13
@Pacerier: (A) przeczytaj pytanie: „bez przeglądarki, kontekst API”. (B) przeglądarki i tak nie konstruują twoich żądań. Robisz to sam, ręcznie. W przeglądarkach nie ma magii.
EML,
12
@BeniBela, prawdopodobnie zaproponuje '()+-./:=wtedy użycie . Jeszcze losowe generowanie podciąg z wyboru jest jeszcze do zrobienia i można to zrobić z jednej linii: while(true){r = rand(); if(data.indexOf(r) === -1){doStuff();break;}}. Sugestia EML (konwersja do base64, aby uniknąć dopasowania podciągów) jest po prostu dziwna, nie wspominając o tym, że wiąże się z niepotrzebnym spadkiem wydajności. I wszystkie kłopoty za nic, ponieważ algorytm jednowierszowy jest równie prosty i prosty. Base64 nie powinien być (ab) używany w ten sposób, ponieważ treść HTTP akceptuje wszystkie 8-bitowe oktety.
Pacerier
31
Ta odpowiedź nie tylko nie dodaje niczego do dyskusji, ale także daje niewłaściwe porady. Po pierwsze, za każdym razem, gdy przesyłamy losowe dane w oddzielnych częściach, zawsze jest możliwe, że wybrana granica będzie obecna w ładunku. JEDYNYM sposobem upewnienia się, że tak się nie stanie, jest zbadanie całej ładowności dla każdej wymyślonej przez nas granicy. Całkowicie niepraktyczne. Po prostu akceptujemy nieskończenie małe prawdopodobieństwo kolizji i przedstawiamy rozsądną granicę, na przykład „--- granica- <UUID tutaj>-granica ---”. Po drugie, zawsze używanie Base64 marnuje przepustowość i zapełnia bufory bez żadnego powodu.
vagelis
92

Nie sądzę, że HTTP jest ograniczony do POST w postaci wieloczęściowej lub w formie x-www-form-urlencoded. Nagłówka Content-Type jest prostopadła do metody HTTP POST (można wypełnić typ odpowiedni dla ciebie MIME). Dzieje się tak również w przypadku typowych aplikacji webowych opartych na reprezentacji HTML (np. Ładunek json stał się bardzo popularny do przesyłania ładunku dla żądań ajax).

Jeśli chodzi o Restful API przez HTTP, najpopularniejszymi typami treści, z którymi się skontaktowałem, są application / xml i application / json.

application / xml:

  • rozmiar danych: XML bardzo szczegółowy, ale zwykle nie stanowi problemu przy użyciu kompresji i przekonaniu, że przypadek dostępu do zapisu (np. przez POST lub PUT) jest znacznie rzadszy niż dostęp do odczytu (w wielu przypadkach stanowi <3% całego ruchu ). Rzadko tam, gdzie musiałem zoptymalizować wydajność zapisu
  • istnienie znaków innych niż ascii: możesz użyć utf-8 jako kodowania w XML
  • istnienie danych binarnych: trzeba by użyć kodowania base64
  • nazwa pliku danych: możesz zamknąć to pole wewnętrzne w formacie XML

application / json

  • rozmiar danych: bardziej kompaktowy mniej niż XML, wciąż tekst, ale można go skompresować
  • znaki inne niż ascii: json to utf-8
  • dane binarne: base64 (zobacz także pytanie json-binary )
  • nazwa pliku: enkapsuluj jako własną sekcję pola wewnątrz json

dane binarne jako zasoby własne

Próbowałbym przedstawić dane binarne jako własny zasób / zasób. Dodaje kolejne połączenie, ale lepiej oddziela rzeczy. Przykładowe obrazy:

POST /images
Content-type: multipart/mixed; boundary="xxxx" 
... multipart data

201 Created
Location: http://imageserver.org/../foo.jpg  

W późniejszych zasobach możesz po prostu wstawić zasób binarny jako link:

<main-resource>
 ...
 <link href="http://imageserver.org/../foo.jpg"/>
</main-resource>
Manuel Aldana
źródło
Ciekawy. Ale kiedy używać application / x-www-form-urlencoded a kiedy multipart / form-data?
maks.
3
application / x-www-form-urlencoded jest domyślnym typem MIME twojego żądania (patrz również w3.org/TR/html401/interact/forms.html#h-17.13.4 ). Używam go do „normalnych” formularzy internetowych. Do API używam application / xml | json. multipart / form-data to dzwonek w myśleniu o załącznikach (w treści odpowiedzi kilka sekcji danych jest połączonych określonym ciągiem granicznym).
manuel aldana
4
Myślę, że OP prawdopodobnie pytał tylko o dwa typy formularzy HTML, ale cieszę się, że zostało to wskazane.
tybro0103
30

Zgadzam się z tym, co powiedział Manuel. W rzeczywistości jego komentarze odnoszą się do tego adresu URL ...

http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4

... który stwierdza:

Typ zawartości „application / x-www-form-urlencoded” jest nieefektywny w przypadku wysyłania dużych ilości danych binarnych lub tekstu zawierającego znaki spoza ASCII. Do przesyłania formularzy zawierających pliki, dane inne niż ASCII i dane binarne należy używać typu treści „multipart / form-data”.

Jednak dla mnie sprowadzałoby się to do obsługi narzędzi / ram.

  • Z jakich narzędzi i struktur oczekuje się, że użytkownicy interfejsu API będą budować swoje aplikacje?
  • Czy mają szkielety lub komponenty, z których mogą korzystać, które faworyzują jedną metodę nad drugą?

Jeśli uzyskasz jasny obraz użytkowników i tego, w jaki sposób będą korzystać z Twojego interfejsu API, pomoże Ci to podjąć decyzję. Jeśli utrudnisz przesyłanie plików użytkownikom interfejsu API, to oni się odejdą, poświęcając dużo czasu na ich obsługę.

Drugi to wsparcie narzędzi, które masz do pisania API i jak łatwo jest dostosować jeden mechanizm przesyłania do drugiego.

Martin Peck
źródło
1
Cześć, czy to oznacza, że ​​za każdym razem, gdy publikujemy coś na serwerze internetowym, musimy wspomnieć, jaki jest typ zawartości, aby poinformować serwer internetowy, czy powinien dekodować dane? Nawet jeśli sami tworzymy żądanie HTTP, MUSIMY wspomnieć o typie treści, prawda?
GMsoF,
2
@GMsoF, jest opcjonalny. Zobacz stackoverflow.com/a/16693884/632951 . Możesz uniknąć używania typu zawartości podczas tworzenia konkretnego żądania dla określonego serwera, aby uniknąć ogólnych kosztów ogólnych.
Pacerier,
2

Mała wskazówka z mojej strony dotycząca przesyłania danych obrazu płótna HTML5:

Pracuję nad projektem dla drukarni i miałem problemy z przesyłaniem obrazów na serwer, które pochodziły z canvaselementu HTML5 . Walczyłem przez co najmniej godzinę i nie udało mi się go poprawnie zapisać obrazu na moim serwerze.

Po ustawieniu contentTypeopcji mojego wywołania jQuery ajax application/x-www-form-urlencodedwszystko poszło we właściwy sposób, a dane zakodowane w base64 zostały poprawnie zinterpretowane i pomyślnie zapisane jako obraz.


Może to komuś pomaga!

Torsten Barthel
źródło
4
Jaki typ treści wysyłał przed zmianą? Ten problem mógł być spowodowany tym, że serwer nie obsługiwał typu zawartości, do którego go wysyłałeś.
catorda
1

Jeśli potrzebujesz użyć Content-Type = x-www-urlencoded-form, NIE używaj FormDataCollection jako parametru: W asp.net Core 2+ FormDataCollection nie ma domyślnych konstruktorów wymaganych przez Formatters. Zamiast tego użyj IFormCollection:

 public IActionResult Search([FromForm]IFormCollection type)
    {
        return Ok();
    }
jahansha
źródło