Inspectable download responses via IDownloadClient and DownloadResult
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 38/100
Piste de recherche
Commencez par le chemin IRestClient.DownloadStreamAsync existant dans RestClient et examinez la manière dont RestResponse est géré pour les téléchargements réussis et échoués. Résolvez les questions ouvertes de l’API concernant IDownloadClient, DownloadResult et DownloadDataAsync, puis vérifiez que le streaming, le comportement existant, l’inspection de la réponse et le chemin de migration documenté correspondent tous à la proposition.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
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.
- Langage dominant
- C#
- Étoiles
- 9.8k
- Forks
- 2.3k
- Merge moyen
- 11 j 21 h
- PR mergées (30 j)
- 2
Préparer son environnement
Lance le conteneur de développement du projet dans votre navigateur, avec votre propre compte GitHub.
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de restsharp/RestSharp
-
RestClientOptions.UserAgent missing from requests with useClientFactory: truePeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouvertebug
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
-
bug
Difficulté 4/5 3-5 jours Accessibilité débutants 48/100
-
feature-request rfc
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 25/100
-
awaiting-feedback feature-request
Difficulté 4/5 3-5 jours Accessibilité débutants 45/100
-
bug
Difficulté 4/5 3-5 jours Accessibilité débutants 45/100
Toutes les issues de restsharp/RestSharp
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
Nimblesite/SharpLsp#468 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
elastic/ecs-dotnet#575 ·
-
V4 alert-rule reads have no scope gate, so any authenticated credential can list alert rulesOuvertealerts bug effort:S P2
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
nightscout/nocturne#2511 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
Les mainteneurs répondent en général sous 2 jours
-
Crash in v3.00Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 62/100
arkane-systems/mousejiggler#140 ·