Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

Inspectable download responses via IDownloadClient and DownloadResult

Offen
#2,386 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Anfängerfreundlichkeit
38/100
Issue-Typ
Feature
Klarheit
Größtenteils klar
Aktivitätsstatus
Ruhig
Tech-Stack
csharp
Bereich
api, backend

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

feature-request rfc

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

  1. Give callers the same inspectability for downloads that ExecuteAsync gives for normal requests: full RestResponse access on both success and failure.
  2. Keep the existing DownloadStreamAsync API working unchanged. No deprecation, no break for current consumers or IRestClient implementers.
  3. Avoid expanding IRestClient further — the interface is intentionally narrow, and DownloadStreamAsync arguably should not have been on it in the first place.
  4. 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:

  • IRestClient is the narrow request/response contract. Streaming is a separate concern.
  • Additive on IRestClient would 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 from result.Response.Content if they need typed errors. Can be added later as a convenience extension.
  • A general OnError interceptor on Interceptor. Useful for cross-cutting concerns (logging, metrics) but doesn't substitute for per-call inspection. Tracked separately if desired.
  • Removing DownloadStreamAsync from IRestClient. Considered and rejected for this iteration — the cost of breaking implementers exceeds the cosmetic benefit. Documented guidance will steer new code toward IDownloadClient.

Open questions

  1. Should IDownloadClient also expose a DownloadDataAsync returning DownloadResult<byte[]>? Probably yes for symmetry, but adds API surface.
  2. Should DownloadResult carry the request URI / final URI (post-redirects) directly, or rely on Response.Request and Response.ResponseUri? Probably the latter to avoid duplication.
  3. Naming: IDownloadClient vs IRestDownloadClient vs putting it on a RestClient.Download property. I lean toward IDownloadClient for brevity.

Migration story

  • Existing code using DownloadStreamAsync keeps compiling and behaving identically.
  • New code that needs response inspection takes a dependency on IDownloadClient (or directly on RestClient) and uses DownloadAsync.
  • 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

In Codespaces öffnen

Startet den Dev-Container des Projekts im Browser, mit Ihrem eigenen GitHub-Konto.

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus restsharp/RestSharp

Alle Issues in restsharp/RestSharp

Ähnliche Issues

Weitere Issues zu C#

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.