Downloader 5.1.0
dotnet add package Downloader --version 5.1.0
NuGet\Install-Package Downloader -Version 5.1.0
<PackageReference Include="Downloader" Version="5.1.0" />
<PackageVersion Include="Downloader" Version="5.1.0" />
<PackageReference Include="Downloader" />
paket add Downloader --version 5.1.0
#r "nuget: Downloader, 5.1.0"
#:package Downloader@5.1.0
#addin nuget:?package=Downloader&version=5.1.0
#tool nuget:?package=Downloader&version=5.1.0
Downloader
🚀 Fast, cross-platform, and reliable multipart downloader in .Net 🚀
Downloader is a modern, fluent, asynchronous, and portable library for .NET, built with testability in mind. It supports multipart downloads with real-time asynchronous progress events. The library is compatible with projects targeting .NET Standard 2.1, .NET 8, and later versions.
Downloader works on Windows, Linux, and macOS.
Note: Support for older versions of .NET was removed in Downloader
v3.2.0. From this version onwards, only.Net 8.0and higher versions are supported.
If you need compatibility with older .NET versions (e.g.,.NET Framework 4.6.1), use Downloaderv3.1.*.
For a complete example, see the Downloader.Sample project in this repository.
Sample Console Application
Key Features
- Simple interface for download requests.
- Asynchronous, non-blocking file downloads.
- Supports all file types (e.g., images, videos, PDFs, APKs).
- Cross-platform support for files of any size.
- Real-time progress updates for each download chunk.
- Downloads files in multiple parts (parallel download).
- Resilient to client-side and server-side errors.
- Configurable
ChunkCountto control download segmentation. - Supports both in-memory and on-disk multipart downloads.
- Parallel saving of chunks directly into the final file (no temporary files).
- Always downloads to a temporary file (configurable extension, default
.download), then renames to the final name on completion. - Always pre-allocates file size before download begins.
- Resume downloads manually by saving and restoring the
DownloadPackageobject. - Automatic resume: when enabled, download metadata is embedded inside the
.downloadfile — no extra files or manual serialization needed. - Provides real-time speed and progress data.
- Asynchronous pause and resume functionality.
- Download files with dynamic speed limits.
- Supports downloading to memory streams (without saving to disk).
- Supports large file downloads and live-streaming (e.g., music playback during download).
- Download a specific byte range from a large file.
- Lightweight, fast codebase with no external dependencies.
- Manage RAM usage during downloads.
- Supports custom
HttpClientorHttpMessageHandlerinjection for advanced scenarios (e.g.,IHttpClientFactory, HTTP caching, custom delegating handlers).
Installation via NuGet
PM> Install-Package Downloader
Installation via the .NET CLI
dotnet add package Downloader
Usage
Step 1: Create a Custom Configuration
Simple Configuration
var downloadOpt = new DownloadConfiguration()
{
ChunkCount = 8, // Number of file parts, default is 1
ParallelDownload = true // Download parts in parallel (default is false)
};
Complex Configuration
Note: Only include the options you need in your application.
var downloadOpt = new DownloadConfiguration()
{
// usually, hosts support max to 8000 bytes, default value is 8000
BufferBlockSize = 10240, // 10KB
// file parts to download, the default value is 1
ChunkCount = 8,
// download speed limited to MaximumBytesPerSecond, default values is zero or unlimited
MaximumBytesPerSecond = 1024*1024*2, // 2MB/s
// the maximum number of times to fail
MaxTryAgainOnFailure = 5,
// release memory buffer after each MaximumMemoryBufferBytes
MaximumMemoryBufferBytes = 1024 * 1024 * 50, // 50MB
// download parts of the file as parallel or not. The default value is false
ParallelDownload = true,
// number of parallel downloads. The default value is the same as the chunk count
ParallelCount = 4,
// timeout (millisecond) per stream block reader, default values is 1000
BlockTimeout = 1000,
// timeout (millisecond) per HttpClientRequest, default values is 100 Seconds
HTTPClientTimeout = 100 * 1000,
// set true if you want to download just a specific range of bytes of a large file
RangeDownload = false,
// floor offset of download range of a large file
RangeLow = 0,
// ceiling offset of download range of a large file
RangeHigh = 0,
// clear package chunks data when download completed with failure, default value is false
ClearPackageOnCompletionWithFailure = true,
// the minimum size of file to chunking or download a file in multiple parts, the default value is 512
MinimumSizeOfChunking = 102400, // 100KB
// the minimum size of a single chunk, default value is 0 equal unlimited
MinimumChunkSize = 10240, // 10KB
// Get on demand downloaded data with ReceivedBytes on downloadProgressChanged event
EnableLiveStreaming = false,
// How to handle existing filename when starting to download?
FileExistPolicy = FileExistPolicy.Delete,
// When enabled, the Downloader appends package metadata to the end of the
// .download file. On the next download attempt, if metadata is found in an
// existing .download file, the download resumes automatically.
EnableAutoResumeDownload = true,
// A temporary extension appended to the real filename while downloading.
// e.g., "file.zip" becomes "file.zip.download" during download.
// The Downloader always uses this extension regardless of EnableAutoResumeDownload.
// When the download completes, the file is renamed back to its final name.
DownloadFileExtension = ".download",
// config and customize request headers
RequestConfiguration =
{
Accept = "*/*",
CookieContainer = cookies,
Headers = ["Accept-Encoding: gzip, deflate, br"], // { your custom headers }
KeepAlive = true, // default value is false
ProtocolVersion = HttpVersion.Version11, // default value is HTTP 1.1
// your custom user agent or your_app_name/app_version.
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
Proxy = new WebProxy() {
Address = new Uri("http://YourProxyServer/proxy.pac"),
UseDefaultCredentials = false,
Credentials = System.Net.CredentialCache.DefaultNetworkCredentials,
BypassProxyOnLocal = true
},
Authorization = new AuthenticationHeaderValue("Bearer", "token");
}
};
Step 2: Create the Download Service
var downloader = new DownloadService(downloadOpt);
Step 3: Handle Download Events
// Provide `FileName` and `TotalBytesToReceive` at the start of each download
downloader.DownloadStarted += OnDownloadStarted;
// Provide any information about chunker downloads,
// like progress percentage per chunk, speed,
// total received bytes and received bytes array to live streaming.
downloader.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged;
// Provide any information about download progress,
// like progress percentage of sum of chunks, total speed,
// average speed, total received bytes and received bytes array
// to live streaming.
downloader.DownloadProgressChanged += OnDownloadProgressChanged;
// Download completed event that can include errors or
// canceled or download completed successfully.
downloader.DownloadFileCompleted += OnDownloadFileCompleted;
Step 4: Start the Download
string file = @"Your_Path\fileName.zip";
string url = @"https://file-examples.com/fileName.zip";
await downloader.DownloadFileTaskAsync(url, file);
Step 4b: Start the download without file name
DirectoryInfo path = new DirectoryInfo("Your_Path");
string url = @"https://file-examples.com/fileName.zip";
// download into "Your_Path\fileName.zip"
await downloader.DownloadFileTaskAsync(url, path);
Step 4c: Download in MemoryStream
// After download completion, it gets a MemoryStream
Stream destinationStream = await downloader.DownloadFileTaskAsync(url);
How to pause and resume downloads quickly
When you want to resume a download quickly after pausing a few seconds. You can call the Pause function of the downloader service. This way, streams stay alive and are only suspended by a locker to be released and resumed whenever you want.
// Pause the download
DownloadService.Pause();
// Resume the download
DownloadService.Resume();
How to stop and resume downloads (manual approach)
The DownloadService class has a property called Package that holds a live snapshot of the download state (chunk positions, URL, file path, etc.). While the download is in progress, this object is updated continuously.
To stop and later resume a download manually, you are responsible for keeping the Package object yourself — either in memory or serialized to disk. The Downloader does not store it for you in this approach.
// 1. Keep a reference to the package before or after stopping:
DownloadPackage pack = downloader.Package;
Stop or cancel the download:
await downloader.CancelAsync();
Resume later — even after restarting the application:
// Pass the same (or deserialized) package to resume from the last position:
await downloader.DownloadFileTaskAsync(pack);
The Package object is lightweight — it contains only the URL, file path, and the position of each chunk (not the downloaded bytes). You can serialize it to JSON or binary (see Serialization section) and restore it at any time.
For more details see the StopResumeDownloadTest method.
Note: If the server does not support HTTP range requests, the download cannot be resumed and will restart from the beginning.
How to automatically resume downloads (recommended)
If you don't want to manage DownloadPackage serialization yourself, enable EnableAutoResumeDownload. The Downloader will then handle everything automatically — no manual serialization or package management is needed.
Configuration:
var downloadOpt = new DownloadConfiguration()
{
EnableAutoResumeDownload = true,
DownloadFileExtension = ".download" // optional, this is the default
};
var downloader = new DownloadService(downloadOpt);
How .download files work:
The Downloader always uses a temporary file with the .download extension (configurable via DownloadFileExtension) — this behavior is independent of EnableAutoResumeDownload. For example, downloading report.pdf creates report.pdf.download on disk. While the download is in progress, only the .download file exists — the user does not see the final filename. When the download completes successfully, the file is renamed to report.pdf.
What EnableAutoResumeDownload adds:
When this option is true, the Downloader appends package metadata to the end of the .download file, immediately after the file data. This metadata enables automatic resume without any work from the caller.
The file structure during download looks like this:
report.pdf.download:
|<---------- File Data (TotalFileSize) ----------><-- Metadata -->|
- File Data region: The actual file content. The file size for this region is pre-allocated at the start.
- Metadata region: The
DownloadPackagestate (includes TotalFileSize and Chunks) is serialized to JSON and then written as binary at the end of the same file.
The metadata grows as the download progresses (more chunk positions are recorded), so the on-disk file size is always TotalFileSize + current metadata size. The metadata only grows — it never shrinks — so no padding or extra management is needed.
On interruption:
If the download is interrupted (crash, network failure, app restart), the .download file remains on disk with both the partial file data and the latest metadata embedded at the end.
On resume:
When you call DownloadFileTaskAsync for the same file again, the Downloader:
- Detects the existing
.downloadfile - Reads the metadata from the end of the file to restore the
DownloadPackagestate - Verifies the server still supports range requests and that the file size has not changed
- Resumes downloading from where each chunk left off
- Falls back to a fresh download if any validation fails
On completion:
When the download finishes successfully:
- The file stream is truncated to
TotalFileSizeusingSetLength(TotalFileSize), which removes the appended metadata - The file is renamed from
report.pdf.downloadtoreport.pdf
The result is a clean final file with no metadata artifacts.
Note: If the server does not support range requests or the remote file size has changed, the Downloader will discard the partial data and start a fresh download.
Fluent download builder usage
For easy and fluent use of the downloader, you can use the DownloadBuilder class. Consider the following examples:
Simple usage:
await DownloadBuilder.New()
.WithUrl(@"https://host.com/test-file.zip")
.WithDirectory(@"C:\temp")
.Build()
.StartAsync();
Complex usage:
IDownload download = DownloadBuilder.New()
.WithUrl(@"https://host.com/test-file.zip")
.WithDirectory(@"C:\temp")
.WithFileName("test-file.zip")
.WithConfiguration(new DownloadConfiguration())
.Build();
download.DownloadProgressChanged += DownloadProgressChanged;
download.DownloadFileCompleted += DownloadFileCompleted;
download.DownloadStarted += DownloadStarted;
download.ChunkDownloadProgressChanged += ChunkDownloadProgressChanged;
await download.StartAsync();
download.Stop(); // cancel current download
Resume the existing download package:
await DownloadBuilder.Build(package).StartAsync();
Resume the existing download package with a new configuration:
await DownloadBuilder.Build(package, config).StartAsync();
var download = DownloadBuilder.New()
.Build()
.WithUrl(url)
.WithFileLocation(path);
await download.StartAsync();
download.Pause(); // pause current download quickly
download.Resume(); // continue current download quickly
Using a Custom HttpClient or HttpMessageHandler
Some scenarios require using a custom HttpClient or HttpMessageHandler — for example, to reuse the connection pool from IHttpClientFactory, add HTTP caching via a DelegatingHandler, or apply custom authentication logic.
The Downloader provides two delegate properties on DownloadConfiguration for this purpose:
Option 1: Provide a fully custom HttpClient
Use CustomHttpClientFactory when you want full control over the HttpClient instance. The Downloader will skip all internal handler and header configuration and use the returned client directly.
var downloadOpt = new DownloadConfiguration()
{
ChunkCount = 8,
ParallelDownload = true,
CustomHttpClientFactory = () => {
// Example: use IHttpClientFactory
return httpClientFactory.CreateClient("MyDownloader");
}
};
var downloader = new DownloadService(downloadOpt);
await downloader.DownloadFileTaskAsync(url, filePath);
Option 2: Provide a custom HttpMessageHandler
Use CustomHttpMessageHandlerFactory when you want to customize only the handler (e.g., for caching or custom SSL), while letting the Downloader still configure default request headers and timeout on the HttpClient.
var downloadOpt = new DownloadConfiguration()
{
ChunkCount = 8,
ParallelDownload = true,
CustomHttpMessageHandlerFactory = () => {
return new SocketsHttpHandler {
MaxConnectionsPerServer = 500,
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
}
};
var downloader = new DownloadService(downloadOpt);
await downloader.DownloadFileTaskAsync(url, filePath);
Using the fluent builder API
Both options are also available through the DownloadBuilder:
// With a custom HttpClient
await DownloadBuilder.New()
.WithUrl(url)
.WithDirectory(@"C:\temp")
.WithHttpClient(() => httpClientFactory.CreateClient("MyDownloader"))
.Build()
.StartAsync();
// With a custom HttpMessageHandler
await DownloadBuilder.New()
.WithUrl(url)
.WithDirectory(@"C:\temp")
.WithHttpMessageHandler(() => new SocketsHttpHandler {
MaxConnectionsPerServer = 500,
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
})
.Build()
.StartAsync();
Note: If both
CustomHttpClientFactoryandCustomHttpMessageHandlerFactoryare set,CustomHttpClientFactorytakes precedence andCustomHttpMessageHandlerFactoryis ignored.
When does the Downloader fail to download in multiple chunks?
Content-Length
If your URL server does not provide the file size in the response header (Content-Length).
The Downloader cannot split the file into multiple parts and continues its work with one chunk.
Accept-Ranges
If the server returns Accept-Ranges: none in the responses header then that means the server does not support download in range and
the Downloader cannot use multiple chunking and continues its work with one chunk.
Content-Range
At first, the Downloader sends a GET request to the server to fetch the file's size in the range.
If the server does not provide Content-Range in the header then that means the server does not support download in range.
Therefore, the Downloader has to continue its work with one chunk.
How to serialize and deserialize the downloader package
Tip: If you use
EnableAutoResumeDownload = true, you do not need to serialize the package yourself — the Downloader handles it automatically by embedding metadata in the.downloadfile. The section below is only relevant if you use the manual resume approach.
The DownloadPackage object holds the download state (URL, file path, chunk positions). You can serialize it to JSON or binary so that you can restore and resume a stopped download later — even after the application restarts.
JSON Serialization
// Serialize
var packageJson = JsonConvert.SerializeObject(package);
// Deserialize
var restoredPack = JsonConvert.DeserializeObject<DownloadPackage>(packageJson);
// Resume
await downloader.DownloadFileTaskAsync(restoredPack);
For more details see the PackageSerializationTest method.
Binary Serialization
To save the package as a binary file, serialize it to JSON first and then write it with BinaryWriter.
NOTE: Do not use BinaryFormatter — it is deprecated and insecure. Reference
🚀 Building a Native AOT Version
This project supports Ahead-of-Time (AOT) compilation, which generates a standalone native executable with faster startup times and reduced memory usage. Follow these steps to build the AOT version.
Prerequisites
- .NET 8.0 SDK or later.
- A supported platform (Windows, Linux, or macOS).
Build Instructions
1. Clone the Repository
git clone https://github.com/bezzad/downloader.git
cd downloader
2. Build the Native AOT Executable
Run the following command to compile the project for your target platform:
Windows (x64):
dotnet publish -r win-x64 -f net8.0 -c Release
Linux (x64):
dotnet publish -r linux-x64 -f net8.0 -c Release
macOS (x64):
dotnet publish -r osx-x64 -f net8.0 -c Release
3. Find the Output
The compiled executable will be located in:
bin/Release/net8.0/<RUNTIME_IDENTIFIER>/publish/
Example for Windows:
bin/Release/net8.0/win-x64/publish/
Instructions for Contributing
Welcome to contribute, feel free to change and open a PullRequest to develop the branch. You can use either the latest version of Visual Studio or Visual Studio Code and .NET CLI for Windows, Mac and Linux.
For GitHub workflow, check out our Git workflow below this paragraph. We are following the excellent GitHub Flow process, and would like to make sure you have all the information needed to be a world-class contributor!
Git Workflow
The general process for working with Downloader is:
- Fork on GitHub
- Make sure your line endings are correctly configured and fix your line endings!
- Clone your fork locally
- Configure the upstream repo (
git remote add upstream git://github.com/bezzad/downloader) - Switch to the latest development branch (e.g. vX.Y.Z, using
git checkout vX.Y.Z) - Create a local branch from that (
git checkout -b myBranch). - Work on your feature
- Rebase if required
- Push the branch up to GitHub (
git push origin myBranch) - Send a Pull Request on GitHub - the PR should target (have as a base branch) the latest development branch (eg
vX.Y.Z) rather thanmaster.
We accept pull requests from the community. But, you should never work on a clone of the master, and you should never send a pull request from the master - always from a branch. Please be sure to branch from the head of the latest vX.Y.Z develop branch (rather than master) when developing contributions.
You can run tests with the Docker Compose file with the following command
docker-compose -p downloader up
Or with docker file
docker build -f ./dockerfile -t downloader-linux .docker run --name downloader-linux-container -d downloader-linux --env=ASPNETCORE_ENVIRONMENT=Development .
Or run the following command to call docker directly
docker run --rm -v ${pwd}:/app --env=ASPNETCORE_ENVIRONMENT=Development -w /app/tests mcr.microsoft.com/dotnet/sdk:6.0 dotnet test ../ --logger:trx
License
Licensed under the terms of the MIT License
Contributors
Thanks go to these wonderful people (List made with contrib. rocks):
<a href="https://github.com/bezzad/downloader/graphs/contributors"> <img alt="downloader contributors" src="https://contrib.rocks/image?repo=bezzad/downloader" /> </a>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
NuGet packages (13)
Showing the top 5 NuGet packages that depend on Downloader:
| Package | Downloads |
|---|---|
|
SquareMinecraftLauncher.Core
BaiBaoStudio |
|
|
Orobouros
A fully-featured and modular online scraper tool. Yes we know the name is spelled wrong. Icon Credit: Hyliian @ DeviantArt |
|
|
OverrideLauncher.Core
新一代 MC 启动器,迅速、快速、轻便! |
|
|
EasyUpdate
Easy Update 提供简单的自动更新服务。 |
|
|
SmartUtilTools
提供的高效工具集,包含加密、JSON处理、时区转换等功能。 |
GitHub repositories (24)
Showing the top 20 popular GitHub repositories that depend on Downloader:
| Repository | Stars |
|---|---|
|
2dust/v2rayN
A GUI client for Windows, Linux and macOS, support Xray and sing-box and others
|
|
|
yaobiao131/downkyicore
哔哩下载姬(跨平台版)downkyi,哔哩哔哩网站视频下载工具,支持批量下载,支持8K、HDR、杜比视界,提供工具箱(音视频提取、去水印等)。
|
|
|
goatcorp/FFXIVQuickLauncher
Custom launcher for FFXIV
|
|
|
ClassIsland/ClassIsland
一款功能强、可定制、跨平台,适用于班级多媒体屏幕的课表信息显示工具,可以一目了然地显示各种信息。
|
|
|
Paving-Base/APK-Installer
An Android Application Installer for Windows
|
|
|
CHKZL/DDTV
可对阿B进行直播多窗口观看、开播提醒、自动录制、合并、转码的跨平台工具
|
|
|
rogerfar/rdt-client
Real-Debrid Client Proxy
|
|
|
Taiizor/Sucrose
Sucrose is a versatile wallpaper engine that brings life to your desktop with a wide range of interactive wallpapers.
|
|
|
jxlpzqc/TMSpeech
腾讯会议摸鱼工具
|
|
|
chickensoft-games/GodotEnv
Manage Godot versions and addons from the command line on Windows, macOS, and Linux.
|
|
|
K12f/BlueCatKoKo
蓝猫KoKo下载器(BlueCatKoKo)是一个免登录,简单易用的桌面端抖音,快手视频下载工具,具有简洁的界面,流畅的操作逻辑。可以下载几乎所有的视频,并输出mp4格式的文件。
|
|
|
HandyOrg/HandyWinGet
GUI for installing apps through WinGet and Creating Yaml file
|
|
|
Ameliorated-LLC/trusted-uninstaller-cli
Core functionality for AME Wizard
|
|
|
LocalizeLimbusCompany/LLC_MOD_Toolbox
模组安装程序
|
|
|
YuukiPS/Launcher-PC
|
|
|
Team-Resurgent/Repackinator
|
|
|
yiikooo/Aurelio
|
|
|
Euterpe-org/Euterpe
Muse Dash Mod Manager
|
|
|
baibao132/SquareMinecraftLauncherCore
|
|
|
ktxiaok/FireAxe
A Left 4 Dead 2 addon manager that supports hierarchical organization, workshop items and collections download, addon enablement management, etc.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 5.1.0 | 95 | 3/11/2026 |
| 5.0.0 | 227 | 3/10/2026 |
| 4.1.1 | 6,972 | 2/10/2026 |
| 4.1.0 | 1,449 | 2/9/2026 |
| 4.0.3 | 91,535 | 8/9/2025 |
| 4.0.2 | 6,833 | 7/12/2025 |
| 4.0.1-beta | 823 | 5/14/2025 |
| 4.0.0-beta | 303 | 5/14/2025 |
| 3.3.4 | 58,073 | 3/10/2025 |
| 3.3.3 | 29,523 | 1/13/2025 |
| 3.3.2 | 404 | 1/13/2025 |
| 3.3.1 | 18,541 | 11/28/2024 |
| 3.3.0 | 3,447 | 11/20/2024 |
| 3.2.1 | 23,289 | 10/4/2024 |
| 3.2.0 | 2,353 | 9/22/2024 |
| 3.1.2 | 46,718 | 6/30/2024 |
| 3.1.0-beta | 924 | 1/2/2024 |
| 3.0.6 | 144,531 | 6/6/2023 |
| 3.0.5 | 570 | 6/3/2023 |
| 3.0.4 | 167,322 | 3/11/2023 |
* Added CustomHttpClientFactory property on DownloadConfiguration to inject a fully custom HttpClient instance (e.g. from IHttpClientFactory). When set, the Downloader bypasses all internal handler and header configuration.
* Added CustomHttpMessageHandlerFactory property on DownloadConfiguration to inject a custom HttpMessageHandler while still letting Downloader configure default headers and timeout.
* Added WithHttpClient() and WithHttpMessageHandler() fluent builder methods on DownloadBuilder for easy factory configuration.
* Added proper HttpClient ownership semantics: externally provided HttpClient instances are never disposed by the Downloader; internally created clients are disposed on cleanup.
* Added proper HttpMessageHandler ownership semantics: when a custom handler factory is used, the handler is not disposed with the HttpClient (disposeHandler: false), keeping ownership with the caller.
* Added SocketClient disposal in AbstractDownloadService.Clear() to prevent HttpClient/socket leaks across download sessions.