Laden...

Grosse Datenmengen (keine Dateien, Länge unbekannt) per HTTP hochladen

Erstellt von Johannäs vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.463 Views
J
Johannäs Themenstarter:in
3 Beiträge seit 2019
vor 5 Jahren
Grosse Datenmengen (keine Dateien, Länge unbekannt) per HTTP hochladen

Ich qäle mich jetzt schon seit Stunden mit dem System.Net.Http.HttpClient rum. Ich möchte größere Datenmengen via HTTP-POST über eine REST-API hochladen. Die Daten liegen nicht als Datei vor und auch deren Länge ist zu Beginn des Uploads nicht bekannt. Via StreamConten() kann ich zwar Daten aus eine Stream hochladen, die Daten werden aber immer komplett irgendwo gepuffert und dann erst hochgeladen. Ich möchte aber, dass die wirklich gestreamt werden, d.h. der HTTP-POST muss mit Transfer-Encoding: "chunked" erfolgen.

Hier ein ganz einfacher Test:

static async Task Main(string[] args)
{
    var httpClient = new System.Net.Http.HttpClient();
    var httpRequestMessage = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, "http://localhost:5678/foo/bar");
    var multipartFormDataContent = new System.Net.Http.MultipartFormDataContent();
    multipartFormDataContent.Add(new System.Net.Http.StringContent("Hello World"));
    var fileStream = File.OpenRead("large.file.gz");
    var gz = new GZipStream(fileStream, CompressionMode.Decompress);
    multipartFormDataContent.Add(new System.Net.Http.StreamContent(gz));
    httpRequestMessage.Content = multipartFormDataContent;
    await httpClient.SendAsync(httpRequestMessage);
}

Heraus kommt dabei:


POST /foo/bar HTTP/1.1
Content-Type: multipart/form-data; boundary="694b4db0-697d-40a8-8da7-57d3651cb60d"
Host: localhost:5678
Content-Length: 109353122
Expect: 100-continue
Connection: Keep-Alive

--694b4db0-697d-40a8-8da7-57d3651cb60d
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data

Hello World
--694b4db0-697d-40a8-8da7-57d3651cb60d
Content-Disposition: form-data

Schon am Content-Length-Feld sieht man, dass der HttpClient den ganze GZipStream komplett in den Speicher geladen haben muss, da GZipStream nich seekable ist und keine Länge zurückliefert.

Hat jemand eine Idee, ob das mit HttpClient überhaupt möglich ist (.Net 4.6+)?

Johannäs

16.806 Beiträge seit 2008
vor 5 Jahren

IIRC brauchst Du dafür eine eigene Implementierung für ein asynchronen Push-Stream.
Push and Pull Streams using HttpClient
Sobald die Content Length gesetzt wird, gibt es kein Streaming mehr. Ohne Content Length gibt es aber kein Partial Serving (oder auch Byte Serving genannt). Du musst Dich also entscheiden.

Dein Code zeigt aber unglückliche Basisfehler mit dem HTTP Client.
Siehe auch YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Es ist aber ratsam gegen eine Rest API nicht mit direkt mit dem HttpClient zu arbeiten, sondern mit Bibliotheken, die einem hier viel abnehmen.
Sehr weit verbreitet ist hier https://github.com/reactiveui/refit.

J
Johannäs Themenstarter:in
3 Beiträge seit 2019
vor 5 Jahren

Danke erstmal für die schnelle Antwort!

Ob ich einen Push-Stream oder Pull-Stream verwende spielt aber keinerlei Rolle. HttpClient liest den Stream komplett in den Speicher bevor er gesendet wird. Auch mit der Push-Stream-Implementierung aus dem verlinkten Artikel sehe ich, dass das Content-Length-Feld gesetzt wird.

Byte-Serving spielt doch eigentlich nur Server-Seitig eine Rolle - wäre mir neu, dass auch ein HTTP-Client einen Upload(!) via Byte-Serving abwickeln kann - ist mir jedenfalls noch nie untergekommen, auch wenns technisch sicherlich möglich ist.

Den gezeigten Code habe ich bewusst auf ein Minimum reduziert und auf Dispose-Calls etc. verzichtet. Bei einem "One-Shot-CLI"-Programm wird sowieso alles beim Exit freigegeben.

Normalerweise verwende ich RestSharp zum Zugriff auf REST-API's. RestSharp kann auch Uploads Streamen - aber auch nur ohne chunked-Transfer-Encoding, d.h. nur für Streams deren Länge bekannt ist.

ReFit kannte ich noch nicht, danke für den Hinweis! Hab es auch gleich ausprobiert: Multipart-Post mit StreamPart als Parameter - es wird aber auch wieder der komplette Stream in den Speicher gelesen und dann gesendet. Unter der Haube verwendet ReFit auch nur HttpClient und StreamContent - kommt also auf das selbe Problem raus.

Ich hab jetzt noch ein wenig rumexperimentiert. Bisher habe ich immer mit einer 100MB-Datei getestet. Verwende ich ein 5GB-Datei scheitere ich an einer OOME bei 32bit, bzw. an einer zu geringen Puffergröße (2147483647) bei 64Bit.

Das waren bisher alles Tests mit .Net 4.7.2, Spaßenshalber habe ich das jetzt auch mal mit .Net Core 2.1 probiert - exakt der selbe Code: Und siehe da - es funktioniert! Ich bekomme ein "Transfer-Encoding: chunked"!

Das Problem liegt also in der .Net 4.7.2-Implementierung von System.Net.Http.

Bleibt die Frage offen, ob man die .Net 4.7.2-Implementierung vielleicht doch irgendwie dazu bewegen kann, via "Transfer-Encoding: chunked" zu senden.

J
Johannäs Themenstarter:in
3 Beiträge seit 2019
vor 5 Jahren

Manchmal ist es vielleicht zu einfach:

httpRequestMessage.Headers.TransferEncodingChunked = true;

...und schon klappt es auch mit .Net 4.7.2. Warum das wie bei .Net Core nicht automatisch geschieht ist mir unklar 😕