Inspectable download responses via IDownloadClient and DownloadResult
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 5/5
- Geschätzter Aufwand
- Über eine Woche
- Anfängerfreundlichkeit
- 38/100
Rechercherichtung
Beginne mit dem bestehenden IRestClient.DownloadStreamAsync-Pfad in RestClient und überprüfe, wie RestResponse bei erfolgreichen und fehlgeschlagenen Downloads behandelt wird. Kläre die offenen API-Fragen zu IDownloadClient, DownloadResult und DownloadDataAsync und verifiziere anschließend, dass Streaming, das bestehende Verhalten, die Antwortinspektion und der dokumentierte Migrationspfad alle mit dem Vorschlag übereinstimmen.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
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.
- Vorherrschende Sprache
- C#
- Sterne
- 9.8k
- Forks
- 2.3k
- Ø Merge
- 11 T. 21 Std.
- Gemergte PRs (30 T.)
- 2
Entwicklungsumgebung
Startet den Dev-Container des Projekts im Browser, mit Ihrem eigenen GitHub-Konto.
- Kein Dockerfile und keine Docker-Compose-Datei
- Hat eine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus restsharp/RestSharp
-
RestClientOptions.UserAgent missing from requests with useClientFactory: trueEvtl. vergeben Ein verknüpfter Pull Request ist offen oder bereits gemergt. Offenbug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
-
Default AutomaticDecompression = All routes responses through the Brotli decoder affected by CVE-2026-111497Evtl. vergeben @K3UL hat das vor 1 Tag übernommen. Offenbug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 35/100
-
bug
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 48/100
-
feature-request rfc
Schwierigkeit 5/5 Über eine Woche Anfängerfreundlichkeit 25/100
-
awaiting-feedback feature-request
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 45/100
Alle Issues in restsharp/RestSharp
Ähnliche Issues
-
Bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 73/100
dotnet/roslynator#1859 ·
Maintainer antworten meist innerhalb von 1 Tag
-
area:jobads-cv BE P3
Schwierigkeit 1/5 1-3 Stunden Anfängerfreundlichkeit 74/100
klasolsson81/jobbliggaren#2240 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
k2-fsa/sherpa-onnx#4100 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
Maintainer antworten meist innerhalb von 1 Tag
-
:watch: Not Triaged
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 82/100
Maintainer antworten meist innerhalb von 1 Tag