| name | network-client-usage |
| description | 引导在 crates/application 层统一使用进程级全局网络客户端(global_client / ClientProvider)与下载管理器(DownloadManager / CoreDownloadService)的规范。使用场景:新增联网的业务服务、改造现有服务使其跟随全局代理、需要创建/查询/取消下载任务、判断某个位置应该直接获取客户端还是注入 provider、审查是否违反“请求时获取、不缓存固定客户端、插件客户端独立”等约定。Also use when writing or reviewing Rust code that touches sealantern_infra::net or sealantern_infra::download so the producer stays on the global proxy path. |
| description_en | Guidance for uniformly using the process-level global network client (global_client / ClientProvider) and download manager (DownloadManager / CoreDownloadService) at the crates/application layer. Use when adding network-facing services, migrating existing services to follow the global proxy, creating/querying/canceling download tasks, deciding whether to fetch a client directly or inject a provider, or reviewing code for the "fetch-per-request, never cache a fixed client, plugin client stays separate" conventions. Also use when writing or reviewing Rust code that touches sealantern_infra::net or sealantern_infra::download so the producer stays on the global proxy path. |
Global Network Client & Download Manager Usage Guide
The repository's process-level networking is managed by crates/infra. Proxy policies
(Adaptive / Preserve / Manual / Disabled) are written into the process-level global network
runtime by the settings layer and the system-proxy polling service. Business code is only
responsible for reading the current global client and issuing requests against it.
Core Principles
- Single source of truth: production code only obtains a client from
global_client() or
global_client_provider(). Never call NetClient::from_config / reqwest::Client::builder
yourself.
- Fetch per request: obtain a client right before each request (or before each download
task). Services must not cache a fixed
NetClient at construction time.
- Inject a provider, not a client: for dependency injection, pass a
ClientProvider
(a functional fetcher). Tests can inject fake providers.
- Plugin exception: plugin networking must keep using the standalone
PluginNetworkClient
(DNS/IP pinning, SSRF protection, request-header allowlist). Do not migrate it to the global client.
- Error propagation:
global_client() returns Result<NetClient, NetError>. Propagate with
?; never use unwrap/expect.
1. How to Use the Global Client
Direct Access (simple call sites)
use sealantern_infra::net::global_client;
async fn fetch_text(url: &str) -> Result<String, NetError> {
let client = global_client()?;
let response = client.get(url)?.send().await?;
let body = response
.text()
.await
.map_err(|error| NetError::Request(format!("failed to read response body: {error}")))?;
Ok(body)
}
Key types and functions:
pub fn global_client() -> Result<NetClient, NetError>;
pub type ClientProvider = Box<dyn Fn() -> Result<NetClient, NetError> + Send + Sync>;
pub fn global_client_provider() -> ClientProvider;
NetClient is a cheap clone (it wraps a reqwest::Client plus a retry policy). Every call
returns the client that matches the current proxy policy. After the proxy settings or the
system proxy change and the runtime rebuilds the client, the next global_client() call
automatically returns the new client.
Inject a Provider (recommended for long-lived services)
use std::sync::Arc;
use sealantern_infra::net::{global_client_provider, ClientProvider, NetClient, NetError};
struct MarketService {
client_provider: ClientProvider,
}
impl MarketService {
fn new() -> Self {
Self { client_provider: global_client_provider() }
}
fn with_provider(client_provider: ClientProvider) -> Self {
Self { client_provider }
}
async fn search(&self, query: &str) -> Result<String, NetError> {
let client = (self.client_provider)()?;
Ok(String::new())
}
}
Inject a fake provider in tests:
let provider: ClientProvider = Box::new(|| {
let client = NetClient::from_config(&ClientConfig::default())?;
Ok(client)
});
let service = MarketService::with_provider(provider);
Anti-Patterns
struct BadService {
client: NetClient,
}
fn new() -> Self {
Self { client: global_client().expect("...") }
}
2. How to Use the Download Manager
infra layer: DownloadManager
use sealantern_infra::download::DownloadManager;
let manager = DownloadManager::instance();
let manager = DownloadManager::with_provider(global_client_provider());
let manager = DownloadManager::new(client);
Task lifecycle:
let id = manager.create("https://example.com/file.zip", "./download/file.zip", 8).await?;
let (id, status) = manager.create_with_handle(url, path, thread_count).await?;
let snapshot = manager.get_progress(id).await;
let all = manager.get_all_progress().await;
manager.cancel(id).await;
let count = manager.task_count().await;
Notes:
create / create_with_handle: url, output path, thread count; thread count must be > 0.
- When the server does not support Range or provides no Content-Length, the manager falls back to
single-threaded streaming download automatically.
- Finished or canceled tasks are removed from the manager to avoid unbounded growth.
Downloader is pub(crate); business code only talks to DownloadManager.
application layer: CoreDownloadService
use sealantern_application::service::CoreDownloadService;
let service = CoreDownloadService::new();
let service = CoreDownloadService::with_provider(provider);
let service = CoreDownloadService::with_manager(manager);
It implements the sealantern_application::port::DownloadService contract:
let id = service.create(DownloadRequest {
url: "...".into(),
save_path: "...".into(),
thread_count: 8,
}).await?;
let info = service.poll(&id).await?;
service.cancel(&id).await?;
Note: the thread-count limit is validated at the application layer
(MAX_DOWNLOAD_THREAD_COUNT = 64); 0 or values above the limit return InvalidInput.
3. Usage Conventions (Checklist)
Verify each item when writing or reviewing code:
References
- Global network runtime:
crates/infra/src/net/runtime.rs
- Download manager:
crates/infra/src/download/{manager.rs,multi.rs,tasks.rs,single.rs}
- Application download service:
application/src/service/download.rs
- Plugin secure client (stays standalone):
crates/infra/src/net/plugin/
中文原版:references/zh-CN.md