0

При отправки запроса на rest веб сайта тяжелого файла упирался в ошибку таймаута "The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing."
Исправил данным образом:

    public static HttpClient _client = HttpClientGenerator(20);
private static HttpClient HttpClientGenerator(int TimeOut)
{
        HttpClientHandler handler = new HttpClientHandler();
        var httpClient = new HttpClient(handler,false);
        httpClient.Timeout = TimeSpan.FromMinutes(TimeOut);
        return httpClient;
}

Но, такой большой таймаут не нужен хочу определять его от размера файла

if (MB < 500)
    _client = HttpClientGenerator(10);
else if (MB < 14000)
    _client = HttpClientGenerator(20);
else
    _client = HttpClientGenerator(30);

Проблема в том что появляется ошибка - System.Net.Http.HttpRequestException: "An error occurred while sending the request."
InvalidOperationException: Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток. Как можно исправить данную проблему?

Добавление к ответу:Изначально это исключение было создано в этом стеке вызовов: [Внешний код] App.Resources.Net.Rest_API.RestService.E24SendFileBase.AnonymousMethod__0(int) в RestService.cs App.Resources.Net.Rest_API.ProgressStreamContent.SerializeToStreamAsync(System.IO.Stream, System.Net.TransportContext) в ProgressStreamContent.cs [Внешний код]

Метод который отправляет данные на веб ресурс:

    public static async Task<string> E24SendFileBase(string file, string fileName, ProgressBar progressBar, System.Windows.Controls.Label label) а
    {
    using (var content = new MultipartFormDataContent()) 
    {
        var progressContent = new ProgressStreamContent(new StreamContent(System.IO.File.OpenRead(file)), progress: progress =&gt; 
        {
            progressBar.Value = progress;
            label.Content = $&quot;{progress} %&quot;;

        }, totalBytesRead: totalBytesRead =&gt;
        {
            label.Content += $&quot; {totalBytesRead} /&quot;;
        }, totalBytes: totalBytes =&gt;
        {
            label.Content += $&quot; {totalBytes}&quot;;
        });

        switch (fileName.Remove(0, fileName.IndexOf(&quot;.&quot;) + 1))  
        {
            case &quot;zip&quot;:
                progressContent.Headers.ContentType = new MediaTypeWithQualityHeaderValue(&quot;application/zip&quot;);
                break;
            case &quot;dt&quot;:
                progressContent.Headers.ContentType = new MediaTypeWithQualityHeaderValue(&quot;application/octet-stream&quot;); 
                break;
            case &quot;1CD&quot;:
                progressContent.Headers.ContentType = new MediaTypeWithQualityHeaderValue(&quot;application/octet-stream&quot;); 
                break;
            default:
                break;
        }


        ulong MB = Convert.ToUInt64(progressContent.Headers.ContentLength) &gt;&gt; 20;

        /*if (MB &lt; 500)
            _client = HttpClientGenerator(10);
        else if (MB &lt; 14000)
            _client = HttpClientGenerator(20);
        else
            _client = HttpClientGenerator(30);*/


        content.Add(progressContent, name: &quot;FILE_BASE&quot;, fileName: $&quot;{fileName}&quot;);


        using (var message = await _client.PostAsync(&quot;https://fake/rest//fake/&quot;, content))
        {
            return message.Content.ReadAsStringAsync().Result; 
        }
    }

Класс ProgressStreamContent

 public class ProgressStreamContent : HttpContent
 {
     private readonly HttpContent _content;
     private readonly Action<int> _progress;
     private readonly Action<string> _totalBytesRead;
     private readonly Action<string> _totalBytes;
     public ProgressStreamContent(HttpContent content, Action<int> progress, Action<string> totalBytesRead, Action<string> totalBytes)
     {
         _content = content;
         _progress = progress;
         _totalBytesRead = totalBytesRead;
         _totalBytes = totalBytes;
     }
 protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
 {
     var buffer = new byte[4096];
     var totalBytesRead = 0L;

     using (var inputStream = await _content.ReadAsStreamAsync())
     {
         var totalBytes = inputStream.Length;
         var bytesRead = 0;

         while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) &gt; 0)
         {
             await stream.WriteAsync(buffer, 0, bytesRead);

             totalBytesRead += bytesRead;
             var progress = (int)((double)totalBytesRead / totalBytes * 100);
             _progress(progress);
             _totalBytesRead(SizeSuffix(totalBytesRead));
             _totalBytes(SizeSuffix(totalBytes));
         }
     }
 }

 protected override bool TryComputeLength(out long length)
 {
     length = _content.Headers.ContentLength.GetValueOrDefault();
     return true;
 }

 static readonly string[] SizeSuffixes =
           { &quot;bytes&quot;, &quot;KB&quot;, &quot;MB&quot;, &quot;GB&quot;, &quot;TB&quot;, &quot;PB&quot;, &quot;EB&quot;, &quot;ZB&quot;, &quot;YB&quot; };

 static string SizeSuffix(Int64 value, int decimalPlaces = 1)
 {
     if (value &lt; 0) { return &quot;-&quot; + SizeSuffix(-value, decimalPlaces); }

     int i = 0;
     decimal dValue = (decimal)value;
     while (Math.Round(dValue, decimalPlaces) &gt;= 1000)
     {
         dValue /= 1024;
         i++;
     }

     return string.Format(&quot;{0:n&quot; + decimalPlaces + &quot;} {1}&quot;, dValue, SizeSuffixes[i]);
 }

}

aepot
  • 49,560
Extro
  • 11
  • 2
    Не нужно плодить HttpClientHandler. Нужно "чинить" HttpClient. 1) пилим TimeoutHandler для поддержки таймаута для операции 2) пилим extension методы для HttpClient чтобы были всякие SendAsync с параметром timeout (или наследуем HttpClient и добавляем что нам надо). Так же решаем и другие неудобства HttpClient – vitidev Mar 15 '24 at 05:38
  • Спасибо большое за решение! Сейчас попробую переделать. – Extro Mar 15 '24 at 05:51
  • Ну это все же костыль. Ведь нужно следить чтобы никто не изменил общий таймаут у клиента. Да еще и ловить нужное исключение. Увы, HttpClient фундаментально кривой – vitidev Mar 15 '24 at 07:12
  • Тогда что можно попробовать вместо него IHttpClientFactory или WebClient ? – Extro Mar 15 '24 at 07:15
  • 1
    @Extro WebClient и прочие HttpWebRequest устарели, умерли и разложились. Вызывающий поток не может получить доступ к данному объекту - это исключение не относится к HttpClient, его вызывает что-то другое, например попытка взаимодействия с UI-объектами из стороннего потока. – aepot Mar 15 '24 at 07:17
  • 1
    Мне непонятно, откуда такая проблема, что таймаута почти в 2 минуты - мало? Вы как-то странно нецелевым образом используете клиент? Пробовали включить HTTP/2? Сжатие трафика? Никаких таймаутов при отправке файлов не должно быть. Надо видеть код, который падает с исключением этим. Версия дотнета и операционной системы какая? – aepot Mar 15 '24 at 07:19
  • @aepot мало конечно. Сервер обычно ограничивает скорость и времени нужно много. А задать общий большой таймаут - в чем смысл тогда вообще таймаута. Плодить несколько клиентов под разные запросы - дичь, менять таймаут перед запросом - дичь. Вот и приходится изворачиваться добавляя методы параметром таймаут. – vitidev Mar 15 '24 at 07:30
  • 1
    @vitidev таймаут при активной передаче данных не вызывается. Таймаут вызывается только при отсутствии передачи данных. Я гигабайты лил на сервер, никаких исключений не наблюдал, на дефолтных настройках клиента. Автор не сообщил, что у него за операционная система и версия дотнета. Если Framework 4.x - ответ очевиден. – aepot Mar 15 '24 at 07:32
  • @aepot Может там такой хитрый сервер, что он что-то с данными предварительно делает, пакует их там не знаю или ещё что-то хитрое. Чем больше запрашиваемые данные - тем дольше он думает перед отправкой. Это бы что-то могло объяснить. Хотя, конечно, хрень какая-то по большому счёту, сервер не должен так себя вести. А паковать данные можно и "на лету". – CrazyElf Mar 15 '24 at 07:40
  • @aepot "Таймаут вызывается только при отсутствии передачи данных" - ничего подобного. Там используется CancellationTokenSource.CancelAfter(_timeout) и ему плевать на передачу данных. Не успела передача - будет исключение. – vitidev Mar 15 '24 at 07:50
  • @vitidev Дайте пожалуйста ссылку на исходники, которые вы смотрели. Интересно, от какой они версии .NET? – CrazyElf Mar 15 '24 at 08:00
  • @CrazyElf Не понимаю просьбы. Есть пакет System.Net.Http (для 4.8 его нужно ставить самому, а в остальном он есть в дистре). в нем используется CancelAfter и никакого различия для фреймворков у него нет. Ну разве что в первой версии что на нюгете нашело от 12года используется таймер + явный CTS.Cancel(). Так что выбирайте в гугле любые исходники. А про "таймаут при неактивности" это ServicePointManager.MaxIdleTime/ConnectionLeaseTimeout – vitidev Mar 15 '24 at 08:11
  • 1
    .NET SDK 8.0.200 Windows 10 – Extro Mar 15 '24 at 09:49
  • 1
    Как я и говорил, вы нарушаете потокобезопастность UI, вместо Action<int> progress используйте Progress<T>, вызывать так: _progress.Report(progress);. Ошибка "Вызывающий поток не может получить доступ к данному объекту" уйдёт. Конструктор progress: new Progress<int>(progress => { progressBar.Value = progress; label.Content = $"{progress} %"; }) должен вызываться в UI потоке. Кстати, вот вам решение по отображению мега-кило-байт https://ru.stackoverflow.com/a/1246708/373567 – aepot Mar 15 '24 at 11:21

0 Answers0