我要做什么:
我正在尝试使用Postman将带有文件和JSON Blob的multipart/form-data
上传到ASP.NET Core 2.2 APIController
,并将文件流式传输到磁盘上的临时文件中,而不是完全导入到其中。由于文件的大小可能很大(20MB-2GB),因此内存不足。我从大文件示例开始遵循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();
}
}
}
我尝试过的事情:
来自Multipart body length limit 16384 exceeded:
MemoryBufferThreshold
MultipartBodyLengthLimit
Content-Type
手动设置为multipart/form-data
来自Multipart body length limit exceeded exception:
ValueLengthLimit
[DisableRequestSizeLimit]
我认为问题出在哪里,但是我不确定解决方法或导致问题的原因: https://github.com/aspnet/AspNetCore/blob/master/src/Http/WebUtilities/src/MultipartReader.cs#L48-L50
似乎我的请求的序言超出了1024 * 16
设置的DefaultHeadersLengthLimit
(16384)的大小限制,但是我不知道为什么会这样。或者,如果序言应该大于该序言,那么如何在不重新实现整个类集的情况下解决该问题,或者如何等待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个小时以上可以找到的所有内容。
答案 0 :(得分:3)
我有类似的问题,我发现问题与 Visual Studio 中的调试断点有关,因为我认为 Visual Studio 尝试读取流,然后尝试中断流,
所以尝试在 Watch 中跳过查看 Request.From 或调试快速查看
答案 1 :(得分:1)
添加[FromForm]解决了我的问题。
例如
[HttpPost]
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]
[Route("far")]
public SingleResponseModel<bool> UploadFar([FromForm] IFormFile file)
{
return _fileUploadService.UploadFar(file);
}
答案 2 :(得分:0)
我已经解决了我的问题。原来那是我使用的URL。
要解决我的问题,我意识到我发送给http端点而不是https端点,从而导致重定向。我将网址从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
的上传由于该重定向而中断。如果有人对为什么有任何想法,我将很高兴学习。
答案 3 :(得分:0)
我使用的是 asp.net core 3.1,我也遇到了这种情况。
当我将一个大文件传递给 api 时,它总是会导致 System.IO.InvalidDataException: Multipart body length limit 16384 exceeded.
现在,我找到了这个问题并阅读了@Sylex 的回答,所以我使用 https://localhost:5001
来请求 api,它有效!!!
知道发生了重定向,于是我去掉了app.UseHttpsRedirection();
中的Startup.cs -> void Configure()
,重新使用了http://localhost:5000/
,现在可以正常请求了。