Search code examples
c#blazorblazor-webassembly.net-8.0

.NET Core 8 - Download file, client does not have the Content-Disposition Header


I have a simple File Upload / Download app with the client in Blazor and the Server as an ASP.NET Core 8 Web API. I set the Content-Disposition in the API with this code:

[HttpGet("{fileName}")]
public async Task<IActionResult> Get(string fileName)
{
     var memoryStream = new MemoryStream();

     try
     {
         if (string.IsNullOrWhiteSpace(fileName))
         {
             throw new ApplicationException("FileName is required as a path parameter.");
         }

         string fullPathName = Path.Combine(_settings.UploadRootFolder!, fileName);

         if (!System.IO.File.Exists(fullPathName))
         {
             throw new ApplicationException($"{fileName} does not exist in {_settings.UploadRootFolder}.");
         }

         var fi = new FileInfo(fullPathName);

         var contentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
         {
             //ModificationDate = fi.LastAccessTimeUtc,
             FileName = fileName,
             FileNameStar = fileName,
             Size = fi.Length,
             //CreationDate = fi.CreationTimeUtc,
             //DispositionType = "attachment",
             //ReadDate = DateTime.UtcNow,
         };

         var mem = System.Diagnostics.Process.GetCurrentProcess().WorkingSet64;
         var memKB = mem / 1024;   
         Response.Headers.ContentDisposition = contentDisposition.ToString();

         _logger.LogInformation($"Attempting to download {fileName} from {_settings.UploadRootFolder} using {memKB.ToString("n0")} KB memory.");

         using (var fileStream = new FileStream(fullPathName, FileMode.Open, FileAccess.Read))
         {
             await fileStream.CopyToAsync(memoryStream, 64 * 1024);
             memoryStream.Position = 0;
             return new FileStreamResult(memoryStream, MimeTypes.GetMimeType(fileName));
         }
     }
     catch (Exception ex)
     {
         _logger.LogError(ex, "Exception trying to download a file");
         return BadRequest();
     }
}

enter image description here

In my client (Blazor), I invoke the download with this code:

private async Task btnDownload_Clicked(string fileName)
{
     if (!string.IsNullOrWhiteSpace(fileName))
     {
         try
         {                
             var response = await _apiClient.GetAsync($"/api/FileDownload/{fileName}");
             response.EnsureSuccessStatusCode();
             //
             // Even though the API successfully set the ContentDisposition header 
             // (verified using debugger) it is null here on the client.
             //
             // var serverFileName = response.Content.Headers.ContentDisposition.FileName; 
             var serverFileName =  fileName;                

             using (var stremRef = new DotNetStreamReference(stream: response.Content.ReadAsStream()))
             {
                 await JS.InvokeVoidAsync("downloadFileFromStream", serverFileName, stremRef);
             }
         }
         catch (Exception ex)
         {
             JS.InvokeVoidAsync("alert", ex.Message);
             throw;
         }
     }
}

I get a NullReferenceException because the fName is NULL because the Headers.ContentDisposition is NULL.

I should be able to set serverFileName to the name found in Content-Disposition, but it is always NULL.

enter image description here

When I comment out the line to get the filename from the header and just use the argument I sent in the path, the download is successful, however, I need to be using Content-Disposition.

Any help is appreciated, my API is running on https://localhost:7283 and my client is running on https://localhost:7203 so they are NOT a Blazor app downloaded from the Client/Server model that VS recommends as a template (separate Client and Server sites seems more real-world).

Thanks for any assistance!


Solution

  • I found the answer - in the API, you need to add the following:

      Response.Headers.AccessControlExposeHeaders = HeaderNames.ContentDisposition;
    

    This makes the download method look like this:

      [HttpGet("{fileName}")]
      public async Task<IActionResult> Get(string fileName)
      {
          var memoryStream = new MemoryStream();
          try
          {
              if (string.IsNullOrWhiteSpace(fileName))
              {
                  throw new ApplicationException("FileName is required as a path parameter.");
              }
              string fullPathName = Path.Combine(_settings.UploadRootFolder!, fileName);
              if (!System.IO.File.Exists(fullPathName))
              {
                  throw new ApplicationException($"{fileName} does not exist in {_settings.UploadRootFolder}.");
              }
              var fi = new FileInfo(fullPathName);
              var contentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
              {
                  ModificationDate = fi.LastAccessTimeUtc,
                  FileName = fileName,
                  FileNameStar = fileName,
                  Size = fi.Length,
                  CreationDate = fi.CreationTimeUtc,
                  DispositionType = "attachment",
                  ReadDate = DateTime.UtcNow,
              };
    
              var mem = System.Diagnostics.Process.GetCurrentProcess().WorkingSet64;
              var memKB = mem / 1024;
              
              Response.Headers.ContentDisposition = contentDisposition.ToString();
              Response.Headers.AccessControlExposeHeaders = HeaderNames.ContentDisposition;
    
              _logger.LogInformation($"Attempting to download {fileName} from {_settings.UploadRootFolder} using {memKB.ToString("n0")} KB memory.");
              using (var fileStream = new FileStream(fullPathName, FileMode.Open, FileAccess.Read))
              {
                  await fileStream.CopyToAsync(memoryStream, 64 * 1024);
                  memoryStream.Position = 0;
                  return new FileStreamResult(memoryStream, MimeTypes.GetMimeType(fileName));
              }
          }
          catch (Exception ex)
          {
              _logger.LogError(ex, "Exception trying to download a file");
              return BadRequest();
          }
      }