[{"data":1,"prerenderedAt":195},["ShallowReactive",2],{"/2025/09/06-download-passthrough-using-dotnet-web-api":3},{"id":4,"title":5,"body":6,"date":186,"description":38,"extension":187,"meta":188,"navigation":189,"path":190,"robots":191,"seo":192,"stem":193,"__hash__":194},"posts/2025/09/06 download-passthrough-using-dotnet-web-api.md","06 Download Passthrough Using Dotnet Web Api",{"type":7,"value":8,"toc":179},"minimark",[9,18,21,25,39,42,50,53,55,63,66,74,85,93,96,104,106,112,123,130,133,141,144,152,154,160,163,165,168,170,176],[10,11,13],"post-title",{":date":12},"date",[14,15,17],"h1",{"id":16},"download-passthrough-using-net-web-api","Download Passthrough using .NET Web API",[19,20],"br",{},[22,23,24],"p",{},"In one of my projects, one of the feature is to be able to upload to and download from existing service/API. Basically, the backend will act as a middle man. The first approach is for the backend to download the file, store it as byte array and then send it down to the frontend like the following:",[26,27,28],"code-block",{},[29,30,35],"pre",{"className":31,"code":33,"language":34},[32],"language-text","public async Task\u003Cbyte[]> DownloadFromServiceAsync(CustomRequest request) {\n  ...\n\n  using (var response = await httpClient.GetAsync(url)) \n  {\n    var byteArray = await response.Content.ReadAsByteArrayAsync();\n\n    return byteArray;\n  }\n}\n","text",[36,37,33],"code",{"__ignoreMap":38},"",[22,40,41],{},"But the method above means the file is retained in the memory on the backend before being sent down to the frontend, thus the server needs to have enough memory to hold the full file size. It gets worse when there are multiple downloads at the same time. The file is then returned to the frontend similar to the following:",[26,43,44],{},[29,45,48],{"className":46,"code":47,"language":34},[32],"[HttpGet]\npublic async Task\u003CIActionResult> DownloadFile(CustomRequest request)\n{\n  var byteArray = await DownloadFromServiceAsync(request);\n\n  return Ok(new CustomResponse() { ByteArray = byteArray });\n}\n",[36,49,47],{"__ignoreMap":38},[22,51,52],{},"Another issue with the method above is the download time. It means frontend has to wait longer since backend has to fully download the file from the existing service before sending it. To solve the issues, the content has to be streamed end-to-end. From existing file service to backend and backend to frontend.",[19,54],{},[56,57,58],"section-title",{},[59,60,62],"h2",{"id":61},"backend-to-frontend","Backend to Frontend",[22,64,65],{},"Apparently the reason why it was done that way is we can't get MemoryStream to work with download. I decided to play with it to try understanding the issues and hopefully find a solution. I started with tackling backend to frontend piece and take care of existing service to backend later. My first try didn't work.",[26,67,68],{},[29,69,72],{"className":70,"code":71,"language":34},[32],"[HttpGet]\npublic async Task\u003CIActionResult> DownloadFile(CustomRequest request)\n{\n  var byteArray = await DownloadFromServiceAsync(request);\n\n  var memoryStream = new MemoryStream(byteArray);\n  memoryStream.Position = 0;\n\n  return Ok(new FileStreamResult(memoryStream, \"application/octet-stream\"));\n}\n",[36,73,71],{"__ignoreMap":38},[22,75,76,77,80,81,84],{},"And it immediately threw an error when I tried it through Swagger UI. Part of the error message said: ",[36,78,79],{},"Timeouts are not supported on this stream",". I found out later that by default, web api will serialize the content as json. I happened to read about ",[36,82,83],{},"FileContentResult"," but it suffers from the same problem. So I decided to experiment a little and finally got it to work by removing the Ok() method, so it becomes:",[26,86,87],{},[29,88,91],{"className":89,"code":90,"language":34},[32],"[HttpGet]\npublic async Task\u003CIActionResult> DownloadFile(CustomRequest request)\n{\n  var byteArray = await DownloadFromServiceAsync(request);\n\n  var memoryStream = new MemoryStream(byteArray);\n  memoryStream.Position = 0;\n\n  return new FileStreamResult(memoryStream, \"application/octet-stream\");\n}\n",[36,92,90],{"__ignoreMap":38},[22,94,95],{},"Alternatively:",[26,97,98],{},[29,99,102],{"className":100,"code":101,"language":34},[32],"[HttpGet]\npublic async Task\u003CIActionResult> DownloadFile(CustomRequest request)\n{\n  var byteArray = await DownloadFromServiceAsync(request);\n\n  var memoryStream = new MemoryStream(byteArray);\n  memoryStream.Position = 0;\n\n  return File(memoryStream, \"application/octet-stream\"));\n}\n",[36,103,101],{"__ignoreMap":38},[19,105],{},[56,107,108],{},[59,109,111],{"id":110},"file-service-to-backend","File Service to Backend",[22,113,114,115,118,119,122],{},"The other half of the solution has something to do with getting the file streamed from existing file service. The ",[36,116,117],{},"httpClient.GetAsync"," has to be replaced with ",[36,120,121],{},"httpClient.GetStreamAsync",". I ended up just passing the stream all the way to the backend endpoint. So instead of:",[26,124,125],{},[29,126,128],{"className":127,"code":33,"language":34},[32],[36,129,33],{"__ignoreMap":38},[22,131,132],{},"It becomes:",[26,134,135],{},[29,136,139],{"className":137,"code":138,"language":34},[32],"public async Task\u003CStream> DownloadFromServiceAsync(CustomRequest request) {\n  ...\n\n  return await httpClient.GetStreamAsync(url);\n}\n",[36,140,138],{"__ignoreMap":38},[22,142,143],{},"And the endpoint code has to be updated:",[26,145,146],{},[29,147,150],{"className":148,"code":149,"language":34},[32],"[HttpGet]\npublic async Task\u003CIActionResult> DownloadFile(CustomRequest request)\n{\n  var stream = await DownloadFromServiceAsync(request);\n\n  return new FileStreamResult(stream, \"application/octet-stream\");\n}\n",[36,151,149],{"__ignoreMap":38},[19,153],{},[56,155,156],{},[59,157,159],{"id":158},"testing","Testing",[22,161,162],{},"Sadly I can't attach a screenshot at the moment, but I tested with 150 MB of text file. Without streaming, the memory used started from 160 MB, shot up to 380 MB after getting the file from file service, and then to 549 MB when Swagger UI finally got hold of the file. With streaming code, since we passed the stream, the memory usage only went from 160 MB to 218 MB for the same file.",[19,164],{},[22,166,167],{},"This is my first attempt to passthrough stream to download file using ASP.NET Web API.",[19,169],{},[56,171,172],{},[59,173,175],{"id":174},"github-repository","GitHub Repository",[22,177,178],{},"(TBA)",{"title":38,"searchDepth":180,"depth":180,"links":181},2,[182,183,184,185],{"id":61,"depth":180,"text":62},{"id":110,"depth":180,"text":111},{"id":158,"depth":180,"text":159},{"id":174,"depth":180,"text":175},"2025-09-06T00:00:00.000Z","md",{},true,"/2025/09/06-download-passthrough-using-dotnet-web-api",null,{"title":5,"description":38},"2025/09/06 download-passthrough-using-dotnet-web-api","Knx9K8NFjbvHqlz9JvgJF4V69Dv17tY3cI402T3sEEc",1785167453248]