Search code examples
c#asp.net-coreasp.net-core-2.0dotnet-httpclientkestrel-http-server

Why do I get a 404 trying to post a large file to a Core Web API


I am very new to file transfer between HttpClient and a Web API, so please excuse any ignorance and guesswork in my code. I have been trying to post a file created with System.IO.Compression.ZipFile to my web API now for about a day, and always I get a 404 response. If I post with an empty stream, the API action method is invoked, so I know the 404 is due to content and not the URI.

This method is in the client WPF application that attempts to post the file:

public async Task PostDirAsync(string localDirPath, string serverDir)
{
    var sourcePath = Path.Combine("Temp", Guid.NewGuid() + ".zip");
    ZipFile.CreateFromDirectory(localDirPath, sourcePath, CompressionLevel.Fastest, true);
    StreamContent streamContent;
    using (var fs = File.Open(sourcePath, FileMode.Open))
    {
        var outStream = new MemoryStream();
        await fs.CopyToAsync(outStream);
        outStream.Position = 0;
        streamContent = new StreamContent(outStream);
    }
    streamContent.Headers.Add("Content-Type", "application/octet-stream");
    var resp = await _client.PostAsync("api/File/PostDir?serverPath={WebUtility.UrlEncode(serverDir)}", streamContent);
}

And this is the action method in the Web API that receives the post, but only if I don't do the outStream.Position = 0; before attempting to post:

[HttpPost("PostDir")]
[DisableRequestSizeLimit]
public async Task<IActionResult> PostDir(string serverPath)
{           
    var zipName = Path.Combine(_config["QuickDrive:TempDir"], Guid.NewGuid() + ".zip");
    using (var ms = new MemoryStream())
    using (var fileStream = System.IO.File.Create(zipName))
    {
        await Request.Body.CopyToAsync(ms);
        ms.Position = 0;
        await ms.CopyToAsync(fileStream);
    }
    return Ok();
}

The action method is invoked and runs without error with an empty stream, but is pretty useless as it writes an empty file. What am I doing wrong?


Solution

  • As mentioned in the comments, your first problem was that the Stream instances involved in the file copying were not being reset using Stream.Position = 0. I know you've made these changes already, but I just want to emphasise that this is a two-part solution.

    So, the second part:

    In your example code, you've added the [DisableRequestSizeLimit] annotation in order to bypass the default ASP.NET Core 2.0+ Kestrel request limits. However, there's also a limit that's imposed by IIS, which is 30MB by default. When this size limit is exceeded, IIS itself generates a 404 response, which is what you're seeing.

    This answer explains how to change this limit using a custom Web.config (included below for completeness):

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <system.webServer>
        <security>
          <requestFiltering>
            <!-- 1 GB -->
            <requestLimits maxAllowedContentLength="1073741824" />
          </requestFiltering>
        </security>
      </system.webServer>
    </configuration>
    

    As somewhat of a side note:

    Unless you have a specific reason to do so, you can avoid the use of MemoryStream in your code and just pass fs directly into new StreamContent(...). You can do something similar with the Request.Body stream and copy that directly into the output FileStream. This would end up with:

    public async Task PostDirAsync(string localDirPath, string serverDir)
    {
        var sourcePath = Path.Combine("Temp", Guid.NewGuid() + ".zip");
        ZipFile.CreateFromDirectory(localDirPath, sourcePath, CompressionLevel.Fastest, true);
    
        var streamContent = new StreamContent(File.Open(sourcePath, FileMode.Open));
        streamContent.Headers.Add("Content-Type", "application/octet-stream");
        var resp = await _client.PostAsync("api/File/PostDir?serverPath={WebUtility.UrlEncode(serverDir)}", streamContent);
    }
    

    And with:

    [HttpPost("PostDir")]
    [DisableRequestSizeLimit]
    public async Task<IActionResult> PostDir(string serverPath)
    {           
        var zipName = Path.Combine(_config["QuickDrive:TempDir"], Guid.NewGuid() + ".zip");
        using (var fileStream = System.IO.File.Create(zipName))
            await Request.Body.CopyToAsync(fileStream );
        return Ok();
    }