Что я пытаюсь сделать:
Я пытаюсь загрузить 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();
}
}
}
Образ конфигурации почтальона (в образе указаны только заданные значения, в заголовках нет значений):
Выход из консоли почтальона:
Вещи, которые я пробовал:
С Превышено ограничение длины тела в 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+ часов.