0
0 Comments

So I have written a simple Azure Function (AF) that accepts (via Http Post method) an IFormCollection, loops through the file collection, pushes each file into an Azure Blob storage container and returns the url to each file.

The function itself works perfectly when I do a single file or multiple file post through Postman using the ‘multipart/form-data’ header. However when I try to post a file through an xUnit test, I get the following error:

System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.

I have searched high and low for a solution, tried different things, namely;

  • Replicating the request object to be as close as possible to Postmans request.
  • Playing around with the ‘boundary’ in the header.
  • Setting ‘RequestFormLimits’ on the function.

None of these have helped so far.

The details are the project are as follows:

Azure Function v3: targeting .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);         }     } </pre></div><!-- /wp:codemirror-blocks/code-block --> <!-- wp:paragraph --><strong>Exception Thrown:</strong><!-- /wp:paragraph --> <!-- wp:paragraph -->The below exception is thrown at the second if statement above, when it tries to process req.Form.Files.Count > 0, i.e.<!-- /wp:paragraph --> <!-- wp:paragraph -->if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) {}<!-- /wp:paragraph --> <!-- wp:codemirror-blocks/code-block {"showPanel":false,"languageLabel":"no","mode":"clike","mime":"text\/x-c++src"} --> 			<div class="wp-block-codemirror-blocks-code-block code-block"><pre>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) </pre></div><!-- /wp:codemirror-blocks/code-block --> <!-- wp:paragraph --><strong>xUnit Test Project:</strong> targeting .netcoreapp3.1<!-- /wp:paragraph --> <!-- wp:paragraph -->Over to the xUnit Test project, basically I am trying to write an integration test. The project references the AF project and has the following classes:<!-- /wp:paragraph --> <!-- wp:paragraph --><em><strong>TestHost.cs</strong></em><!-- /wp:paragraph --> <!-- wp:codemirror-blocks/code-block {"showPanel":false,"languageLabel":"no","mode":"clike","mime":"text\/x-c++src"} --> 			<div class="wp-block-codemirror-blocks-code-block code-block"><pre>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());         }     } } </pre></div><!-- /wp:codemirror-blocks/code-block --> <!-- wp:paragraph --><em><strong>TestCollection.cs</strong></em><!-- /wp:paragraph --> <!-- wp:codemirror-blocks/code-block {"showPanel":false,"languageLabel":"no","mode":"clike","mime":"text\/x-c++src"} --> 			<div class="wp-block-codemirror-blocks-code-block code-block"><pre>[CollectionDefinition(Name)] public class TestCollection : ICollectionFixture<TestHost> {      public const string Name = nameof(TestCollection);  } </pre></div><!-- /wp:codemirror-blocks/code-block --> <!-- wp:paragraph --><em><strong>HttpRequestFactory.cs</strong></em>: To create Http Post Request<!-- /wp:paragraph --> <!-- wp:codemirror-blocks/code-block {"showPanel":false,"languageLabel":"no","mode":"clike","mime":"text\/x-c++src"} --> 			<div class="wp-block-codemirror-blocks-code-block code-block"><pre>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

And Finally the Test class:

[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}";
    }
}

The test seems to be hitting the function and req.ContentLength does have a value. Considering this, could it have something to do with the way the File Streams are being managed? Perhaps not the right way?

Any inputs on this would be greatly appreciated.

Thanks

Anonymous Asked question May 13, 2021