Inspectable download responses via IDownloadClient and DownloadResult
還沒有人認領這個 Issue。
評估
研究方向
從 RestClient 中現有的 IRestClient.DownloadStreamAsync 路徑開始,檢查成功和失敗的下載是如何處理 RestResponse 的。解決有關 IDownloadClient、DownloadResult 和 DownloadDataAsync 的未決 API 問題,然後驗證 streaming、現有行為、回應檢查以及文件化的遷移路徑是否都與提案一致。
由索引模型根據 Issue 內容生成。
描述
Background
IRestClient.DownloadStreamAsync returns Task<Stream?>. On failure it either throws (when ThrowOnAnyError = true) or returns null — in both cases the caller loses access to the RestResponse: status code, headers, response body, error details. This makes downloads materially less usable than ExecuteAsync, where the full RestResponse is always available for inspection.
This has been a recurring pain point. The most explicit attempt to address it was #2128, which added an Action<RestResponse> error-handler overload. That PR is being closed in favour of this design proposal because the callback shape isn't the right primitive — see below.
Goals
- Give callers the same inspectability for downloads that
ExecuteAsyncgives for normal requests: fullRestResponseaccess on both success and failure. - Keep the existing
DownloadStreamAsyncAPI working unchanged. No deprecation, no break for current consumers orIRestClientimplementers. - Avoid expanding
IRestClientfurther — the interface is intentionally narrow, andDownloadStreamAsyncarguably should not have been on it in the first place. - Preserve streaming semantics (
HttpCompletionOption.ResponseHeadersRead, no buffered byte materialisation on success).
Proposal
New result type
public sealed record DownloadResult(Stream? Stream, RestResponse Response) {
public bool IsSuccess => Stream != null && Response.IsSuccessStatusCode;
}
Response is always present and inspectable: StatusCode, Headers, ContentHeaders (including Content-Type, Content-Disposition/filename, Content-Length), ErrorMessage, ErrorException, and — on failure — any body content the server returned (commonly a problem-details JSON).
Stream is non-null only on a successful 2xx response. The caller is responsible for disposing it, same as today.
New companion interface
public interface IDownloadClient {
Task<DownloadResult> DownloadAsync(RestRequest request, CancellationToken cancellationToken = default);
}
RestClient implements both IRestClient and IDownloadClient. Existing extension methods and call sites continue to work.
Rationale for a separate interface rather than adding to IRestClient:
IRestClientis the narrow request/response contract. Streaming is a separate concern.- Additive on
IRestClientwould still break every custom implementation of the interface. - This mirrors the BCL pattern of
IDisposable+IAsyncDisposable— narrow interfaces, opt-in capability.
Existing API is reimplemented in terms of the new path
// Inside RestClient
public async Task<Stream?> DownloadStreamAsync(RestRequest request, CancellationToken ct = default) {
var result = await DownloadAsync(request, ct).ConfigureAwait(false);
if (!result.IsSuccess && Options.ThrowOnAnyError) result.Response.ThrowIfError();
return result.Stream;
}
DownloadDataAsync (an extension over DownloadStreamAsync) is unaffected. We can also add a DownloadDataAsync variant returning DownloadResult for parity, but that can come later.
Usage examples
Inline inspection — the case #2128 was trying to solve:
var result = await client.DownloadAsync(request);
if (!result.IsSuccess) {
throw new MyDomainException(result.Response.StatusCode, result.Response.Content);
}
using var stream = result.Stream!;
await stream.CopyToAsync(targetFile);
Reading response headers for filename / content-type (raised by @DontEatRice in #2128):
var result = await client.DownloadAsync(request);
var filename = result.Response.ContentHeaders?
.FirstOrDefault(h => h.Name == \"Content-Disposition\")?.Value as string;
var contentType = result.Response.ContentType;
Polite retry on 503 with Retry-After (raised by @marcoburato-ecutek in #2128):
var result = await client.DownloadAsync(request);
if (result.Response.StatusCode == HttpStatusCode.ServiceUnavailable) {
var retryAfter = result.Response.Headers?.FirstOrDefault(h => h.Name == \"Retry-After\")?.Value;
// ...
}
Out of scope (for this issue)
- Deserialising the error body to a typed shape via a generic parameter (
DownloadAsync<TError>). Error shapes vary per endpoint; callers can deserialise themselves fromresult.Response.Contentif they need typed errors. Can be added later as a convenience extension. - A general
OnErrorinterceptor onInterceptor. Useful for cross-cutting concerns (logging, metrics) but doesn't substitute for per-call inspection. Tracked separately if desired. - Removing
DownloadStreamAsyncfromIRestClient. Considered and rejected for this iteration — the cost of breaking implementers exceeds the cosmetic benefit. Documented guidance will steer new code towardIDownloadClient.
Open questions
- Should
IDownloadClientalso expose aDownloadDataAsyncreturningDownloadResult<byte[]>? Probably yes for symmetry, but adds API surface. - Should
DownloadResultcarry the request URI / final URI (post-redirects) directly, or rely onResponse.RequestandResponse.ResponseUri? Probably the latter to avoid duplication. - Naming:
IDownloadClientvsIRestDownloadClientvs putting it on aRestClient.Downloadproperty. I lean towardIDownloadClientfor brevity.
Migration story
- Existing code using
DownloadStreamAsynckeeps compiling and behaving identically. - New code that needs response inspection takes a dependency on
IDownloadClient(or directly onRestClient) and usesDownloadAsync. - Docs gain a short "Inspectable downloads" section showing when to reach for
IDownloadClient.
Tracking: closes #2128 in spirit. Cross-references the earlier design discussion in that PR's thread, which substantially informed this proposal.
- 主要語言
- C#
- 星號
- 9.8k
- 分支
- 2.3k
- 平均合併
- 11 天 21 小時
- 30 天內合併 PR
- 2
環境準備
在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
restsharp/RestSharp 的其他 Issue
-
RestClientOptions.UserAgent missing from requests with useClientFactory: true可能已有人在做 關聯的 PR 仍在進行中或已合併。 未關閉bug
難度 2/5 1-3 小時 新手友好度 72/100
-
bug
難度 4/5 3-5 天 新手友好度 48/100
-
feature-request rfc
難度 5/5 一週以上 新手友好度 25/100
-
awaiting-feedback feature-request
難度 4/5 3-5 天 新手友好度 45/100
-
bug
難度 4/5 3-5 天 新手友好度 45/100
查看 restsharp/RestSharp 的全部 Issue
相似的 Issue
-
dev/release: verify-release-candidate.sh runs 0 Java tests with Debian's Maven可能已有人在做 @kou 今天認領。 未關閉Type: bug
難度 2/5 1-3 小時 新手友好度 72/100
apache/arrow-adbc#4947 ·
維護者通常 1 天內回覆
-
:watch: Not Triaged CAC
難度 2/5 1-3 小時 新手友好度 78/100
dotnet/AspNetCore.Docs#38001 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 76/100
Nimblesite/SharpLsp#468 ·
維護者通常 1 天內回覆
-
難度 1/5 1 小時以內 新手友好度 88/100
elastic/ecs-dotnet#575 ·
-
alerts bug effort:S P2
難度 2/5 1-3 小時 新手友好度 76/100
nightscout/nocturne#2511 ·
維護者通常 1 天內回覆