From e7a94996017f1f84d2c849dd878a93aca33224e8 Mon Sep 17 00:00:00 2001 From: 265866 Date: Mon, 1 Jun 2026 19:52:44 -0700 Subject: [PATCH] Stream asset downloads to avoid OutOfMemoryException on large files ExportAssetDownloader used HttpClient.GetAsync(url, ct), which defaults to HttpCompletionOption.ResponseContentRead and buffers the entire response body into memory before returning. Downloading a large attachment therefore tried to grow a single in-memory buffer to the full file size and threw OutOfMemoryException (HttpContent.LoadIntoBufferAsync -> GrowAndWrite). Pass HttpCompletionOption.ResponseHeadersRead so the body is streamed straight to disk via CopyToAsync in bounded chunks, matching how DiscordClient already issues its requests. --- .../Exporting/ExportAssetDownloader.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs index eb44d4a9..badf681e 100644 --- a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs +++ b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -63,8 +64,15 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse) await Http.ResiliencePipeline.ExecuteAsync( async innerCancellationToken => { - // Download the file - using var response = await Http.Client.GetAsync(url, innerCancellationToken); + // Download the file. + // Use ResponseHeadersRead so the response body is streamed to disk instead of + // being fully buffered into memory first, which can cause an OutOfMemoryException + // on large attachments. + using var response = await Http.Client.GetAsync( + url, + HttpCompletionOption.ResponseHeadersRead, + innerCancellationToken + ); await using var output = File.Create(filePath); await response.Content.CopyToAsync(output, innerCancellationToken); },