Skip to content

Commit cd4c434

Browse files
Prerelease 107.2 (#1730)
* #1725 set the timeout status correctly * #1727 use the HttpClient base address if it is set * Streaming API support #1672 #1674
1 parent 077b8da commit cd4c434

21 files changed

+355
-213
lines changed

.github/workflows/codacy-analysis.yml

-54
This file was deleted.

.github/workflows/codeql-analysis.yml

-54
This file was deleted.

RestSharp.sln

+33
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Serializers
2929
EndProject
3030
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Serializers.Xml", "src\RestSharp.Serializers.Xml\RestSharp.Serializers.Xml.csproj", "{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}"
3131
EndProject
32+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.Legacy", "test\RestSharp.Tests.Legacy\RestSharp.Tests.Legacy.csproj", "{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}"
33+
EndProject
3234
Global
3335
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3436
Debug.Appveyor|Any CPU = Debug.Appveyor|Any CPU
@@ -348,6 +350,36 @@ Global
348350
{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x64.Build.0 = Release|Any CPU
349351
{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x86.ActiveCfg = Release|Any CPU
350352
{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x86.Build.0 = Release|Any CPU
353+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU
354+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU
355+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU
356+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU
357+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU
358+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU
359+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU
360+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU
361+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU
362+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU
363+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
364+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
365+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|ARM.ActiveCfg = Debug|Any CPU
366+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|ARM.Build.0 = Debug|Any CPU
367+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
368+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
369+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|x64.ActiveCfg = Debug|Any CPU
370+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|x64.Build.0 = Debug|Any CPU
371+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|x86.ActiveCfg = Debug|Any CPU
372+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Debug|x86.Build.0 = Debug|Any CPU
373+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
374+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|Any CPU.Build.0 = Release|Any CPU
375+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|ARM.ActiveCfg = Release|Any CPU
376+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|ARM.Build.0 = Release|Any CPU
377+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
378+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
379+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|x64.ActiveCfg = Release|Any CPU
380+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|x64.Build.0 = Release|Any CPU
381+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|x86.ActiveCfg = Release|Any CPU
382+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0}.Release|x86.Build.0 = Release|Any CPU
351383
EndGlobalSection
352384
GlobalSection(SolutionProperties) = preSolution
353385
HideSolutionNode = FALSE
@@ -365,5 +397,6 @@ Global
365397
{6D7D1D60-4473-4C52-800C-9B892C6640A5} = {9051DDA0-E563-45D5-9504-085EBAACF469}
366398
{E6D94C12-9AD7-46E6-AB62-3676F25FDE51} = {9051DDA0-E563-45D5-9504-085EBAACF469}
367399
{4A35B1C5-520D-4267-BA70-2DCEAC0A5662} = {8C7B43EB-2F93-483C-B433-E28F9386AD67}
400+
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0} = {9051DDA0-E563-45D5-9504-085EBAACF469}
368401
EndGlobalSection
369402
EndGlobal

docs/usage.md

+28
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,34 @@ var statusCode = client.PostJsonAsync("orders", request, cancellationToken);
360360

361361
The same two extensions also exist for `PUT` requests (`PutJsonAsync`);
362362

363+
### JSON streaming APIs
364+
365+
For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync<T>`, which returns an `IAsyncEnumerable<T>`:
366+
367+
```csharp
368+
public async IAsyncEnumerable<SearchResponse> SearchStream(
369+
[EnumeratorCancellation] CancellationToken cancellationToken = default
370+
) {
371+
var response = _client.StreamJsonAsync<TwitterSingleObject<SearchResponse>>(
372+
"tweets/search/stream", cancellationToken
373+
);
374+
375+
await foreach (var item in response.WithCancellation(cancellationToken)) {
376+
yield return item.Data;
377+
}
378+
}
379+
```
380+
381+
The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string.
382+
383+
### Downloading binary data
384+
385+
There are two functions that allow you to download binary data from the remote API.
386+
387+
First, there's `DownloadDataAsync`, which returns `Task<byte[]`. It will read the binary response to the end, and return the whole binary content as a byte array. It works well for downloading smaller files.
388+
389+
For larger responses, you can use `DownloadStreamAsync` that returns `Task<Stream>`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk.
390+
363391
## Blazor support
364392

365393
Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose.

src/RestSharp/.nvmrc

-1
This file was deleted.

src/RestSharp/Extensions/ResponseStatusExtensions.cs

-35
This file was deleted.

src/RestSharp/Request/InvalidRequestException.cs

-22
This file was deleted.

src/RestSharp/Request/RequestContent.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ void AddPostParameters(ParametersCollection? postParameters) {
164164
var formContent = new FormUrlEncodedContent(
165165
_request.Parameters
166166
.Where(x => x.Type == ParameterType.GetOrPost)
167-
.Select(x => new KeyValuePair<string, string>(x.Name!, x.Value!.ToString()!))
167+
.Select(x => new KeyValuePair<string, string>(x.Name!, x.Value!.ToString()!))!
168168
);
169169
Content = formContent;
170170
}

src/RestSharp/Response/RestResponse.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ CancellationToken cancellationToken
7070
return request.AdvancedResponseWriter?.Invoke(httpResponse) ?? await GetDefaultResponse().ConfigureAwait(false);
7171

7272
async Task<RestResponse> GetDefaultResponse() {
73-
var readTask = request.ResponseWriter == null ? ReadResponse() : ReadAndConvertResponse();
74-
using var stream = await readTask.ConfigureAwait(false);
73+
var readTask = request.ResponseWriter == null ? ReadResponse() : ReadAndConvertResponse();
74+
#if NETSTANDARD
75+
using var stream = await readTask.ConfigureAwait(false);
76+
#else
77+
await using var stream = await readTask.ConfigureAwait(false);
78+
#endif
7579

7680
var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false);
7781
var content = bytes == null ? null : httpResponse.GetResponseString(bytes, encoding);
@@ -109,7 +113,11 @@ async Task<RestResponse> GetDefaultResponse() {
109113
Task<Stream?> ReadResponse() => httpResponse.ReadResponse(cancellationToken);
110114

111115
async Task<Stream?> ReadAndConvertResponse() {
116+
#if NETSTANDARD
112117
using var original = await ReadResponse().ConfigureAwait(false);
118+
#else
119+
await using var original = await ReadResponse().ConfigureAwait(false);
120+
#endif
113121
return request.ResponseWriter!(original!);
114122
}
115123
}

src/RestSharp/RestClient.Async.cs

+9-16
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@ public async Task<RestResponse> ExecuteAsync(RestRequest request, CancellationTo
3737
)
3838
.ConfigureAwait(false)
3939
: AddError(response, internalResponse.Exception, internalResponse.TimeoutToken);
40-
4140

4241
response.Request = request;
4342
response.Request.IncreaseNumAttempts();
44-
43+
4544
return Options.ThrowOnAnyError ? ThrowIfError(response) : response;
4645
}
4746

@@ -104,34 +103,28 @@ record InternalResponse(HttpResponseMessage? ResponseMessage, Uri Url, Exception
104103
if (response.ResponseMessage == null) return null;
105104

106105
if (request.ResponseWriter != null) {
106+
#if NETSTANDARD
107107
using var stream = await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false);
108+
#else
109+
await using var stream = await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false);
110+
#endif
108111
return request.ResponseWriter(stream!);
109112
}
110113

111114
return await response.ResponseMessage.ReadResponse(cancellationToken).ConfigureAwait(false);
112115
}
113116

114-
/// <summary>
115-
/// A specialized method to download files.
116-
/// </summary>
117-
/// <param name="request">Pre-configured request instance.</param>
118-
/// <param name="cancellationToken"></param>
119-
/// <returns>The downloaded file.</returns>
120-
[PublicAPI]
121-
public async Task<byte[]?> DownloadDataAsync(RestRequest request, CancellationToken cancellationToken = default) {
122-
using var stream = await DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false);
123-
return stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false);
124-
}
125-
126117
static RestResponse AddError(RestResponse response, Exception exception, CancellationToken timeoutToken) {
127118
response.ResponseStatus = exception is OperationCanceledException
128-
? timeoutToken.IsCancellationRequested ? ResponseStatus.TimedOut : ResponseStatus.Aborted
119+
? TimedOut() ? ResponseStatus.TimedOut : ResponseStatus.Aborted
129120
: ResponseStatus.Error;
130121

131122
response.ErrorMessage = exception.Message;
132123
response.ErrorException = exception;
133124

134125
return response;
126+
127+
bool TimedOut() => timeoutToken.IsCancellationRequested || exception.Message.Contains("HttpClient.Timeout");
135128
}
136129

137130
internal static RestResponse ThrowIfError(RestResponse response) {
@@ -140,7 +133,7 @@ internal static RestResponse ThrowIfError(RestResponse response) {
140133

141134
return response;
142135
}
143-
136+
144137
static HttpMethod AsHttpMethod(Method method)
145138
=> method switch {
146139
Method.Get => HttpMethod.Get,

src/RestSharp/RestClient.cs

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public RestClient(HttpClient httpClient, RestClientOptions? options = null, bool
8585
Options = options ?? new RestClientOptions();
8686
CookieContainer = new CookieContainer();
8787
_disposeHttpClient = disposeHttpClient;
88+
if (httpClient.BaseAddress != null && Options.BaseUrl == null) {
89+
Options.BaseUrl = httpClient.BaseAddress;
90+
}
8891

8992
ConfigureHttpClient(HttpClient);
9093
}

0 commit comments

Comments
 (0)