InvalidDataException: превышено ограничение длины тела, состоящее из 16384 - PullRequest
0 голосов
/ 09 апреля 2019

Что я пытаюсь сделать:

Я пытаюсь загрузить multipart/form-data с файлом и BLOB-объектом JSON с помощью Postman в ASP.NET Core 2.2 APIController и передать файл во временный файл на диске, а не полностью в память из-за файлы, вероятно, потенциально большого размера (20 МБ - 2 ГБ). Я следовал за обоими примерами из https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.2, начиная с примера с большим файлом, но я также пытался протестировать пример с маленьким файлом с той же ошибкой, с похожими, но разными трассировками стека. Сервер использует Kestrel.

Трассировка стека в примере большого файла (пойман в отладчике):

Exception has occurred: CLR/System.IO.InvalidDataException
Exception thrown: 'System.IO.InvalidDataException' in System.Private.CoreLib.dll: 'Multipart body length limit 16384 exceeded.'
   at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.UpdatePosition(Int32 read)
   at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.<ReadAsync>d__36.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions.<DrainAsync>d__3.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.WebUtilities.MultipartReader.<ReadNextSectionAsync>d__20.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at LookupServiceAPI.Helpers.FileStreamingHelper.<StreamFile>d__1.MoveNext() in <hidden-path-to-project>\Helpers\FileStreamingHelper.cs:line 35

Трассировка стека примера небольшого файла (возвращается в ответе, не попадает ни в какие точки останова или в ловушку исключений отладчика):

System.IO.InvalidDataException: Multipart body length limit 16384 exceeded.
   at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.UpdatePosition(Int32 read)
   at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool`1 bytePool, Nullable`1 limit, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.WebUtilities.MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Features.FormFeature.InnerReadFormAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProviderFactory.AddValueProviderAsync(ValueProviderFactoryContext context)
   at Microsoft.AspNetCore.Mvc.ModelBinding.CompositeValueProvider.CreateAsync(ActionContext actionContext, IList`1 factories)
   at Microsoft.AspNetCore.Mvc.ModelBinding.CompositeValueProvider.CreateAsync(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Вот мой основной код контроллера и вспомогательные классы для примера с большим файлом:

FileStreamingHelper.cs

using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;

namespace LookupServiceAPI.Helpers
{
    public static class FileStreamingHelper
    {
        private static readonly FormOptions _defaultFormOptions = new FormOptions();

        public static async Task<FormValueProvider> StreamFile(this HttpRequest request, Stream targetStream)
        {
            if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
            {
                throw new Exception($"Expected a multipart request, but got {request.ContentType}");
            }

            // Used to accumulate all the form url encoded key value pairs in the 
            // request.
            var formAccumulator = new KeyValueAccumulator();

            var boundary = request.GetMultipartBoundary();
            var reader = new MultipartReader(boundary, request.Body);
            reader.BodyLengthLimit = Int32.MaxValue;
            reader.HeadersLengthLimit = Int32.MaxValue;
            var section = await reader.ReadNextSectionAsync(); //EXCEPTION HERE
            while (section != null)
            {
                ContentDispositionHeaderValue contentDisposition;
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

                if (hasContentDispositionHeader)
                {
                    if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                        await section.Body.CopyToAsync(targetStream);
                    }
                    else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                    {
                        // Content-Disposition: form-data; name="key"
                        //
                        // value

                        // Do not limit the key name length here because the 
                        // multipart headers length limit is already in effect.
                        var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                        var encoding = GetEncoding(section);
                        using (var streamReader = new StreamReader(
                            section.Body,
                            encoding,
                            detectEncodingFromByteOrderMarks: true,
                            bufferSize: 1024,
                            leaveOpen: true))
                        {
                            // The value length limit is enforced by MultipartBodyLengthLimit
                            var value = await streamReader.ReadToEndAsync();
                            if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                            {
                                value = String.Empty;
                            }
                            formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key

                            if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                            {
                                throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                            }
                        }
                    }
                }

                // Drains any remaining section body that has not been consumed and
                // reads the headers for the next section.
                section = await reader.ReadNextSectionAsync();
            }

            // Bind form data to a model
            var formValueProvider = new FormValueProvider(
                BindingSource.Form,
                new FormCollection(formAccumulator.GetResults()),
                CultureInfo.CurrentCulture);

            return formValueProvider;
        }

        private static Encoding GetEncoding(MultipartSection section)
        {
            MediaTypeHeaderValue mediaType;
            var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
            // UTF-7 is insecure and should not be honored. UTF-8 will succeed in 
            // most cases.
            if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding) || mediaType.Encoding == null)
            {
                return Encoding.UTF8;
            }
            return mediaType.Encoding;
        }
    }
}

MultipartRequestHelper.cs

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace LookupServiceAPI.Helpers
{
    public static class MultipartRequestHelper
    {
        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                   && contentDisposition.DispositionType.Equals("form-data")
                   && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                   && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                   && contentDisposition.DispositionType.Equals("form-data")
                   && (!string.IsNullOrEmpty(contentDisposition.FileName.ToString())
                       || !string.IsNullOrEmpty(contentDisposition.FileNameStar.ToString()));
        }
    }
}

Минимальный контроллер:

[Route("api/v0.1/data/excel")]
[ApiController]
public class DataExcelController : ControllerBase
{
    [HttpPost, DisableRequestSizeLimit]
    public async Task<IActionResult> ImportExcel()
    {
        var processID = Guid.NewGuid();
        FormValueProvider multipartContent;
        string tempFilePath = Path.GetTempPath() + processID;
        using(var tempStream = System.IO.File.OpenWrite(tempFilePath))
        {
            multipartContent = await Request.StreamFile(tempStream);
        }
        /** Other unnecessary code **/
        return Ok();
    }
}

Startup.cs

namespace LookupServiceAPI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.Configure<FormOptions>(x =>
            {
                x.MultipartHeadersLengthLimit = Int32.MaxValue;
                x.MultipartBoundaryLengthLimit = Int32.MaxValue;
                x.MultipartBodyLengthLimit = Int64.MaxValue;
                x.ValueLengthLimit = Int32.MaxValue;
                x.BufferBodyLengthLimit = Int64.MaxValue;
                x.MemoryBufferThreshold = Int32.MaxValue;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

Образ конфигурации почтальона (в образе указаны только заданные значения, в заголовках нет значений): Postman Configuration Image

Выход из консоли почтальона: Postman Console Output

Вещи, которые я пробовал:

С Превышено ограничение длины тела в 16384 :

  • Набор MemoryBufferThreshold
  • Набор MultipartBodyLengthLimit
  • Убедитесь, что в заголовках, настроенных почтальоном, Content-Type не установлено вручную multipart/form-data

С Превышено ограничение длины части тела :

  • Набор ValueLengthLimit
  • Используется [DisableRequestSizeLimit]

Где, я думаю, проблема в том, но я не уверен в обходном пути или в чем причина проблемы: https://github.com/aspnet/AspNetCore/blob/master/src/Http/WebUtilities/src/MultipartReader.cs#L48-L50

Кажется, что преамбула для моего запроса выходит за пределы размера 1024 * 16 (16384), установленного для DefaultHeadersLengthLimit, но я понятия не имею, почему это может иметь место. Или, если преамбула должна быть больше этой, как обойти ее, не перевоплощая весь набор классов или не ожидая, пока Microsoft выпустит исправление, которое, похоже, не идет по конвейеру: https://github.com/aspnet/Mvc/issues/7019 https://github.com/aspnet/HttpAbstractions/issues/736

Очевидно, кто-то исправил их проблему, которая была очень похожа на мою (https://github.com/aspnet/Mvc/issues/5128#issuecomment-307675219) здесь: https://github.com/aspnet/Mvc/issues/5128#issuecomment-307962922, но я не могу понять, как выяснить, если это даже относится.

Надеюсь, этого достаточно. Если нет, пожалуйста, дайте мне знать, что вам нужно, и я с удовольствием предоставлю это или опробую любые предложения. Я застрял, исследуя это и пробуя все, что я могу найти, в течение 6+ часов.

1 Ответ

0 голосов
/ 09 апреля 2019

Я решил свой вопрос. Оказывается, это был URL, который я использовал.

Чтобы исправить мою проблему, я понял, что отправляю на конечную точку http вместо конечной точки https, вызывая перенаправление. Я изменил свой URL с http://localhost:5000/ на https://localhost:5001/, и все сразу заработало.

Интересно, что это вызвало проблему и в cURL, когда журнал выглядел следующим образом:

== Info: Connected to localhost (::1) port 5000 (#0)
=> Send header, 257 bytes (0x101)
0000: POST /api/v0.1/data/excel HTTP/1.1
0024: Host: localhost:5000
003a: User-Agent: curl/7.64.0
0053: Accept: */*
0060: cache-control: no-cache
0079: Content-Length: 13286446
0093: Content-Type: multipart/form-data; boundary=--------------------
00d3: ----7b12fc7773ed7878
00e9: Expect: 100-continue
00ff: 
== Info: Expire in 1000 ms for 0 (transfer 0xa6aa80)
<= Recv header, 33 bytes (0x21)
0000: HTTP/1.1 307 Temporary Redirect
<= Recv header, 37 bytes (0x25)
0000: Date: Tue, 09 Apr 2019 18:04:24 GMT
<= Recv header, 17 bytes (0x11)
0000: Server: Kestrel
<= Recv header, 19 bytes (0x13)
0000: Content-Length: 0
<= Recv header, 54 bytes (0x36)
0000: Location: https://localhost:5001/api/v0.1/data/excel
== Info: HTTP error before end of send, stop sending
<= Recv header, 2 bytes (0x2)
0000: 
== Info: Closing connection 0

Изменение конечной точки также исправлено.

Не знаю, почему загрузка multipart/form-data прерывается из-за этого перенаправления. Если у кого-то есть идеи, почему, я был бы рад узнать.

...