【问题标题】:Testing a Multipart file upload Azure Function测试分段文件上传 Azure 函数
【发布时间】:2021-05-13 09:49:44
【问题描述】:

所以我编写了一个简单的 Azure 函数 (AF),它接受(通过 Http Post 方法)一个 IFormCollection,循环遍历文件集合,将每个文件推送到 Azure Blob 存储容器中,并将 URL 返回到每个文件。

当我使用“multipart/form-data”标头通过 Postman 发布单个文件或多个文件时,该功能本身可以完美运行。但是,当我尝试通过 xUnit 测试发布文件时,出现以下错误:

System.IO.InvalidDataException : 超过多部分正文长度限制 16384。

我已经到处寻找解决方案,尝试了不同的方法,即;

  • 复制请求对象以尽可能接近 Postman 请求。
  • 在标题中使用“边界”。
  • 在函数上设置“RequestFormLimits”。

到目前为止,这些都没有帮助。

具体项目如下:

Azure Function v3: 面向 .netcoreapp3.1

Startup.cs

public class Startup : FunctionsStartup
{
    public IConfiguration Configuration { get; private set; }

    public override void Configure(IFunctionsHostBuilder builder)
    {
        var x = builder;

        InitializeConfiguration(builder);

        builder.Services.AddSingleton(Configuration.Get<UploadImagesAppSettings>());

        builder.Services.AddLogging();

        builder.Services.AddSingleton<IBlobService,BlobService>();
    }

    private void InitializeConfiguration(IFunctionsHostBuilder builder)
    {
        var executionContextOptions = builder
            .Services
            .BuildServiceProvider()
            .GetService<IOptions<ExecutionContextOptions>>()
            .Value;

        Configuration = new ConfigurationBuilder()
            .SetBasePath(executionContextOptions.AppDirectory)
            .AddJsonFile("appsettings.json")
            .AddJsonFile("appsettings.Development.json", optional: true)
            .AddEnvironmentVariables()
            .Build();
    }
}

UploadImages.cs

    public class UploadImages
    {
        private readonly IBlobService BlobService;


        public UploadImages(IBlobService blobService)
        {
            BlobService = blobService;
        }

        [FunctionName("UploadImages")]
        [RequestFormLimits(ValueLengthLimit = int.MaxValue, 
            MultipartBodyLengthLimit = 60000000, ValueCountLimit = 10)]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
        {
            List<Uri> returnUris = new List<Uri>();

            if (req.ContentLength == 0)
            {
                string badResponseMessage = $"Request has no content";
                return new BadRequestObjectResult(badResponseMessage);
            }

            if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) 
            {
                foreach (var file in req.Form.Files)
                {
                    if (!file.IsValidImage()) 
                    {
                        string badResponseMessage = $"{file.FileName} is not a valid/accepted Image file";
                        return new BadRequestObjectResult(badResponseMessage);
                    }

                    var uri = await BlobService.CreateBlobAsync(file);
                    if (uri == null)
                    {
                        return new ObjectResult($"Could not blob the file {file.FileName}.");
                    }

                    returnUris.Add(uri);
                }
            }

            if (!returnUris.Any()) 
            {
                return new NoContentResult();
            }

            return new OkObjectResult(returnUris);
        }
    }

抛出异常:

下面的异常在上面的第二个 if 语句中抛出,当它尝试处理 req.Form.Files.Count > 0 时,即

if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) {}

Message: 
    System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
  Stack Trace: 
    MultipartReaderStream.UpdatePosition(Int32 read)
    MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
    StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool`1 bytePool, Nullable`1 limit, CancellationToken cancellationToken)
    MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken)
    FormFeature.InnerReadFormAsync(CancellationToken cancellationToken)
    FormFeature.ReadForm()
    DefaultHttpRequest.get_Form()
    UploadImages.Run(HttpRequest req) line 42
    UploadImagesTests.HttpTrigger_ShouldReturnListOfUploadedUris(String fileNames)

xUnit 测试项目: 针对 .netcoreapp3.1

到 xUnit Test 项目,基本上我正在尝试编写一个集成测试。项目引用AF项目,有以下类:

TestHost.cs

public class TestHost
{
    public TestHost()
    {
        var startup = new TestStartup();
        var host = new HostBuilder()
            .ConfigureWebJobs(startup.Configure)
            .ConfigureServices(ReplaceTestOverrides)
            .Build();

        ServiceProvider = host.Services;
    }

    public IServiceProvider ServiceProvider { get; }

    private void ReplaceTestOverrides(IServiceCollection services)
    {
        // services.Replace(new ServiceDescriptor(typeof(ServiceToReplace), testImplementation));
    }

    private class TestStartup : Startup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            SetExecutionContextOptions(builder);
            base.Configure(builder);
        }

        private static void SetExecutionContextOptions(IFunctionsHostBuilder builder)
        {
            builder.Services.Configure<ExecutionContextOptions>(o => o.AppDirectory = Directory.GetCurrentDirectory());
        }
    }
}

TestCollection.cs

[CollectionDefinition(Name)]
public class TestCollection : ICollectionFixture<TestHost>
{ 
    public const string Name = nameof(TestCollection);

}

HttpRequestFactory.cs:创建 Http Post 请求

public static class HttpRequestFactory
{
    public static DefaultHttpRequest Create(string method, string contentType, Stream body) 
    {
        var request = new DefaultHttpRequest(new DefaultHttpContext());
        var contentTypeWithBoundary = new MediaTypeHeaderValue(contentType) 
        {
            Boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}"
        };

        var boundary = MultipartRequestHelper.GetBoundary(
            contentTypeWithBoundary, (int)body.Length);

        request.Method = method;
        request.Headers.Add("Cache-Control", "no-cache");
        request.Headers.Add("Content-Type", contentType);
        request.ContentType = $"{contentType}; boundary={boundary}";
        request.ContentLength = body.Length;
        request.Body = body;

        return request;
    }

    private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
    {
        var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
        if (string.IsNullOrWhiteSpace(boundary.Value))
        {
            throw new InvalidDataException("Missing content-type boundary.");
        }

        if (boundary.Length > lengthLimit)
        {
            throw new InvalidDataException(
                $"Multipart boundary length limit {lengthLimit} exceeded.");
        }

        return boundary.Value;
    }

}

The MultipartRequestHelper.cs class is available here

最后是测试类:

[Collection(TestCollection.Name)]
public class UploadImagesTests
{
    readonly UploadImages UploadImagesFunction;

    public UploadImagesTests(TestHost testHost)
    {
        UploadImagesFunction = new UploadImages(testHost.ServiceProvider.GetRequiredService<IBlobService>());
    }

    [Theory]
    [InlineData("testfile2.jpg")]
    public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
    {
        var formFile = GetFormFile(fileNames);
        var fileStream = formFile.OpenReadStream();
        var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
        var response = (OkObjectResult)await UploadImagesFunction.Run(request);
        //fileStream.Close();
        Assert.True(response.StatusCode == StatusCodes.Status200OK);
    }

    private static IFormFile GetFormFile(string fileName)
    {
        string fileExtension = fileName.Substring(fileName.IndexOf('.') + 1);
        string fileNameandPath = GetFilePathWithName(fileName);
        IFormFile formFile;
        var stream = File.OpenRead(fileNameandPath);

        switch (fileExtension)
        {
            case "jpg":
                formFile = new FormFile(stream, 0, stream.Length,
                    fileName.Substring(0, fileName.IndexOf('.')),
                    fileName)
                {
                    Headers = new HeaderDictionary(),
                    ContentType = "image/jpeg"
                };
                break;

            case "png":
                formFile = new FormFile(stream, 0, stream.Length,
                    fileName.Substring(0, fileName.IndexOf('.')),
                    fileName)
                {
                    Headers = new HeaderDictionary(),
                    ContentType = "image/png"
                };
                break;

            case "pdf":
                formFile = new FormFile(stream, 0, stream.Length,
                    fileName.Substring(0, fileName.IndexOf('.')),
                    fileName)
                {
                    Headers = new HeaderDictionary(),
                    ContentType = "application/pdf"
                };
                break;

            default:
                formFile = null;
                break;
        }

        return formFile;
    }

    private static string GetFilePathWithName(string filename)
    {
        var outputFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        return $"{outputFolder.Substring(0, outputFolder.IndexOf("bin"))}testfiles\\{filename}";
    }
}

测试似乎正在执行该函数,并且 req.ContentLength 确实有一个值。考虑到这一点,它是否与文件流的管理方式有关?也许不是正确的方法?

对此的任何意见将不胜感激。

谢谢

更新 1

根据this 的帖子,我还尝试在 Azure 函数和/或测试项目的启动中设置 ValueLengthLimit 和 MultipartBodyLengthLimit,而不是在 Azure 函数上设置属性。然后异常变为:

“内流位置意外改变”

然后,我将测试项目中的 fileStream 位置设置为 SeekOrigin.Begin。我开始遇到同样的错误:

“超过多部分正文长度限制 16384。”

【问题讨论】:

    标签: azure http-post multipartform-data asp.net-core-3.1 iformfile


    【解决方案1】:

    我骑了 50 公里的自行车,睡了个好觉,但我终于想通了 :-)。

    Azure 函数 (AF) 接受 HttpRequest 对象作为名称为“req”的参数,即

    公共异步任务运行( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)

    HttpRequest对象中files对象的层次结构(连同参数名称)如下:

    • HttpRequest -> 请求
      • FormCollection -> 表单
        • FormFileCollection -> 文件

    这是 AF 接受的内容,可以使用 req.Form.Files 访问文件集合

    在我的测试用例中,我尝试将文件流发布到 Azure 函数,而不是发布 FormCollection 对象。

    var formFile = GetFormFile(fileNames);
    var fileStream = formFile.OpenReadStream();
    var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
    

    因此,req.Form 具有无法解释的 Stream 值,并且 req.Form.Files 引发了异常。

    为了纠正这个问题,我必须做以下事情:

    • 还原在 UPDATE 1 中所做的所有更改。这意味着我从 Startup 文件中删除了“RequestFormLimits”设置,并将它们作为函数 Run 方法的属性保留。
    • 实例化一个 FormFileCollection 对象并将 IFormFile 添加到其中
    • 使用此 FormFileCollection 作为参数来实例化一个 FormCollection 对象。
    • 将 FormCollection 添加到请求对象。

    为了实现上述目的,我必须对代码进行以下更改。

    更改 HttpRequestFactory 中的“创建”方法

        public static DefaultHttpRequest Create(string method, string contentType, FormCollection formCollection) 
        {
            var request = new DefaultHttpRequest(new DefaultHttpContext());
            var boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}";
    
            request.Method = method;
            request.Headers.Add("Cache-Control", "no-cache");
            request.Headers.Add("Content-Type", contentType);
            request.ContentType = $"{contentType}; boundary={boundary}";
            request.Form = formCollection;
            
    
            return request;
        }
    

    添加私有静态 GetFormFiles() 方法

    我编写了一个额外的 GetFormFiles() 方法,该方法调用现有的 GetFormFile() 方法,实例化一个 FormFileCollection 对象并将 IFormFile 添加到其中。该方法又返回一个 FormFileCollection。

        private static FormFileCollection GetFormFiles(string fileNames) 
        {
            var formFileCollection = new FormFileCollection();
            foreach (var file in fileNames.Split(','))
            {
                formFileCollection.Add(GetFormFile(file));
            }
            return formFileCollection;
        }
    

    更改测试方法

    测试方法调用 GetFormFiles() 获取 FormFileCollection 然后 使用此 FormFileCollection 作为参数实例化 FormCollection 对象,然后将 FormCollection 对象作为参数传递给 HttpRequest 对象,而不是传递 Stream。

        [Theory]
        [InlineData("testfile2.jpg")]
        public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
        {
            var formFiles = GetFormFiles(fileNames);
    
            var formCollection = new FormCollection(null, formFiles);
            var request = HttpRequestFactory.Create("POST", "multipart/form-data", formCollection);
            var response = (OkObjectResult) await UploadImagesFunction.Run(request);
    
            Assert.True(response.StatusCode == StatusCodes.Status200OK);
        }
    

    所以最终问题不在于“RequestFormLimits”,而在于我在 POST 消息中提交的数据类型。

    我希望这个答案能为遇到相同错误消息的人提供不同的视角。

    干杯。

    【讨论】:

      猜你喜欢
      • 2021-01-23
      • 2021-12-18
      • 2017-06-27
      • 2021-03-09
      • 1970-01-01
      • 2015-11-29
      • 1970-01-01
      • 1970-01-01
      • 2020-09-04
      相关资源
      最近更新 更多