tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ApiClient.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Net;
6using System.Net.Http;
7using System.Net.Http.Headers;
8using System.Net.Mime;
9using System.Text;
10using System.Threading;
11using System.Threading.Tasks;
12using System.Web;
13
14using Microsoft.AspNetCore.Http.Connections;
15using Microsoft.AspNetCore.SignalR.Client;
16using Microsoft.Extensions.DependencyInjection;
17using Microsoft.Extensions.Logging;
18using Microsoft.Net.Http.Headers;
19
20using Newtonsoft.Json;
21using Newtonsoft.Json.Converters;
22using Newtonsoft.Json.Serialization;
23
30
32{
35 {
40 static readonly HttpMethod HttpPatch = new("PATCH");
41
43 public Uri Url { get; }
44
47 {
48 get => headers;
49 set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
50 }
51
53 public TimeSpan Timeout
54 {
55 get => httpClient.Timeout;
56 set => httpClient.Timeout = value;
57 }
58
62 static readonly JsonSerializerSettings SerializerSettings = new()
63 {
64 ContractResolver = new CamelCasePropertyNamesContractResolver(),
65 Converters = new[]
66 {
67 new VersionConverter(),
68 },
69 };
70
75
79 readonly List<IRequestLogger> requestLoggers;
80
84 readonly List<HubConnection> hubConnections;
85
90
94 readonly SemaphoreSlim semaphoreSlim;
95
99 readonly bool authless;
100
105
110
116 static void HandleBadResponse(HttpResponseMessage response, string json)
117 {
118 ErrorMessageResponse? errorMessage = null;
119 try
120 {
121 // check if json serializes to an error message
122 errorMessage = JsonConvert.DeserializeObject<ErrorMessageResponse>(json, SerializerSettings);
123 }
124 catch (JsonException)
125 {
126 }
127
128#pragma warning disable IDE0010 // Add missing cases
129 switch (response.StatusCode)
130#pragma warning restore IDE0010 // Add missing cases
131 {
132 case HttpStatusCode.Unauthorized:
133 throw new UnauthorizedException(errorMessage, response);
134 case HttpStatusCode.InternalServerError:
135 throw new ServerErrorException(errorMessage, response);
136 case HttpStatusCode.NotImplemented:
137 // unprocessable entity
138 case (HttpStatusCode)422:
139 throw new MethodNotSupportedException(errorMessage, response);
140 case HttpStatusCode.NotFound:
141 case HttpStatusCode.Gone:
142 case HttpStatusCode.Conflict:
143 throw new ConflictException(errorMessage, response);
144 case HttpStatusCode.Forbidden:
145 throw new InsufficientPermissionsException(response);
146 case HttpStatusCode.ServiceUnavailable:
147 throw new ServiceUnavailableException(response);
148 case HttpStatusCode.RequestTimeout:
149 throw new RequestTimeoutException(response);
150 case (HttpStatusCode)429:
151 throw new RateLimitException(errorMessage, response);
152 default:
153 if (errorMessage?.ErrorCode == ErrorCode.ApiMismatch)
154 throw new VersionMismatchException(errorMessage, response);
155
156 throw new ApiConflictException(errorMessage, response);
157 }
158 }
159
168 public ApiClient(
170 Uri url,
171 ApiHeaders apiHeaders,
173 bool authless)
174 {
175 this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
176 Url = url ?? throw new ArgumentNullException(nameof(url));
177 headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
178 this.tokenRefreshHeaders = tokenRefreshHeaders;
179 this.authless = authless;
180
181 requestLoggers = new List<IRequestLogger>();
182 hubConnections = new List<HubConnection>();
183 semaphoreSlim = new SemaphoreSlim(1);
184 }
185
187 public async ValueTask DisposeAsync()
188 {
189 List<HubConnection> localHubConnections;
190 lock (hubConnections)
191 {
192 if (disposed)
193 return;
194
195 disposed = true;
196
197 localHubConnections = [.. hubConnections];
198 hubConnections.Clear();
199 }
200
201 await ValueTaskExtensions.WhenAll(hubConnections.Select(connection => connection.DisposeAsync()));
202
203 httpClient.Dispose();
204 semaphoreSlim.Dispose();
205 }
206
208 public ValueTask<TResult> Create<TResult>(string route, CancellationToken cancellationToken)
209 => RunRequest<object, TResult>(route, new object(), HttpMethod.Put, null, false, cancellationToken);
210
212 public ValueTask<TResult> Read<TResult>(string route, CancellationToken cancellationToken)
213 => RunRequest<object, TResult>(route, null, HttpMethod.Get, null, false, cancellationToken);
214
216 public ValueTask<TResult> Update<TResult>(string route, CancellationToken cancellationToken)
217 => RunRequest<object, TResult>(route, new object(), HttpMethod.Post, null, false, cancellationToken);
218
220 public ValueTask<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken)
221 where TBody : class
222 => RunRequest<TBody, TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
223
225 public ValueTask Patch(string route, CancellationToken cancellationToken) => RunRequest(route, HttpPatch, null, false, cancellationToken);
226
228 public ValueTask Update<TBody>(string route, TBody body, CancellationToken cancellationToken)
229 where TBody : class
230 => RunResultlessRequest(route, body, HttpMethod.Post, null, false, cancellationToken);
231
233 public ValueTask<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken)
234 where TBody : class
235 => RunRequest<TBody, TResult>(route, body, HttpMethod.Put, null, false, cancellationToken);
236
238 public ValueTask Delete(string route, CancellationToken cancellationToken)
239 => RunRequest(route, HttpMethod.Delete, null, false, cancellationToken);
240
242 public ValueTask<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
243 where TBody : class
244 => RunRequest<TBody, TResult>(route, body, HttpMethod.Put, instanceId, false, cancellationToken);
245
247 public ValueTask<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken)
248 => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, false, cancellationToken);
249
251 public ValueTask<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
252 where TBody : class
253 => RunRequest<TBody, TResult>(route, body, HttpMethod.Post, instanceId, false, cancellationToken);
254
256 public ValueTask Delete(string route, long instanceId, CancellationToken cancellationToken)
257 => RunRequest(route, HttpMethod.Delete, instanceId, false, cancellationToken);
258
260 public ValueTask Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
261 where TBody : class
262 => RunResultlessRequest(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
263
265 public ValueTask<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken)
266 => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
267
269 public ValueTask<TResult> Delete<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
270 where TBody : class
271 => RunRequest<TBody, TResult>(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
272
274 public ValueTask<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken)
275 => RunRequest<object, TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
276
278 public ValueTask<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken)
279 => RunRequest<object, TResult>(route, new object(), HttpPatch, instanceId, false, cancellationToken);
280
282 public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
283
285 public ValueTask<Stream> Download(FileTicketResponse ticket, CancellationToken cancellationToken)
286 {
287 if (ticket == null)
288 throw new ArgumentNullException(nameof(ticket));
289
290 return RunRequest<Stream>(
291 $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}",
292 null,
293 HttpMethod.Get,
294 null,
295 false,
296 cancellationToken);
297 }
298
300 public async ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken)
301 {
302 if (ticket == null)
303 throw new ArgumentNullException(nameof(ticket));
304
305 MemoryStream? memoryStream = null;
306 if (uploadStream == null)
307 memoryStream = new MemoryStream();
308
309 using (memoryStream)
310 {
311 var streamContent = new StreamContent(uploadStream ?? memoryStream);
312 try
313 {
314 await RunRequest<object>(
315 $"{Routes.Transfer}?ticket={HttpUtility.UrlEncode(ticket.FileTicket)}",
316 streamContent,
317 HttpMethod.Put,
318 null,
319 false,
320 cancellationToken)
321 .ConfigureAwait(false);
322 streamContent = null;
323 }
324 finally
325 {
326 streamContent?.Dispose();
327 }
328 }
329 }
330
336 public async ValueTask<bool> RefreshToken(CancellationToken cancellationToken)
337 {
338 if (tokenRefreshHeaders == null)
339 return false;
340
341 var startingToken = headers.Token;
342 await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
343 try
344 {
345 if (startingToken != headers.Token)
346 return true;
347
348 var token = await RunRequest<object, TokenResponse>(Routes.ApiRoot, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false);
349 headers = new ApiHeaders(headers.UserAgent!, token);
350 }
351 finally
352 {
353 semaphoreSlim.Release();
354 }
355
356 return true;
357 }
358
360 public async ValueTask<IAsyncDisposable> CreateHubConnection<THubImplementation>(
361 THubImplementation hubImplementation,
362 IRetryPolicy? retryPolicy,
363 Action<ILoggingBuilder>? loggingConfigureAction,
364 CancellationToken cancellationToken)
365 where THubImplementation : class
366 {
367 if (hubImplementation == null)
368 throw new ArgumentNullException(nameof(hubImplementation));
369
370 retryPolicy ??= new InfiniteThirtySecondMaxRetryPolicy();
371
372 var wrappedPolicy = new ApiClientTokenRefreshRetryPolicy(this, retryPolicy);
373
374 HubConnection? hubConnection = null;
375 var hubConnectionBuilder = new HubConnectionBuilder()
376 .AddNewtonsoftJsonProtocol(options =>
377 {
378 options.PayloadSerializerSettings = SerializerSettings;
379 })
380 .WithAutomaticReconnect(wrappedPolicy)
381 .WithUrl(
382 new Uri(Url, Routes.JobsHub),
383 HttpTransportType.ServerSentEvents,
384 options =>
385 {
386 options.AccessTokenProvider = async () =>
387 {
388 // DCT: None available.
389 if (Headers.Token == null
390 || (Headers.Token.ParseJwt().ValidTo <= DateTime.UtcNow
391 && !await RefreshToken(CancellationToken.None)))
392 {
393 _ = hubConnection!.StopAsync(); // DCT: None available.
394 return null;
395 }
396
397 return Headers.Token.Bearer;
398 };
399
400 options.CloseTimeout = Timeout;
401
402 Headers.SetHubConnectionHeaders(options.Headers);
403 });
404
405 if (loggingConfigureAction != null)
406 hubConnectionBuilder.ConfigureLogging(loggingConfigureAction);
407
408 async ValueTask<HubConnection> AttemptConnect()
409 {
410 hubConnection = hubConnectionBuilder.Build();
411 try
412 {
413 hubConnection.Closed += async (error) =>
414 {
415 if (error is HttpRequestException httpRequestException)
416 {
417 // .StatusCode isn't in netstandard but fuck the police
418 var property = error.GetType().GetProperty("StatusCode");
419 if (property != null)
420 {
421 var statusCode = (HttpStatusCode?)property.GetValue(error);
422 if (statusCode == HttpStatusCode.Unauthorized
423 && !await RefreshToken(CancellationToken.None))
424 _ = hubConnection!.StopAsync();
425 }
426 }
427 };
428
429 hubConnection.ProxyOn(hubImplementation);
430
431 Task startTask;
432 lock (hubConnections)
433 {
434 if (disposed)
435 throw new ObjectDisposedException(nameof(ApiClient));
436
437 hubConnections.Add(hubConnection);
438 startTask = hubConnection.StartAsync(cancellationToken);
439 }
440
441 await startTask;
442
443 return hubConnection;
444 }
445 catch
446 {
447 bool needsDispose;
448 lock (hubConnections)
449 needsDispose = hubConnections.Remove(hubConnection);
450
451 if (needsDispose)
452 await hubConnection.DisposeAsync();
453 throw;
454 }
455 }
456
457 return await WrapHubInitialConnectAuthRefresh(AttemptConnect, cancellationToken);
458 }
459
471#pragma warning disable CA1506 // TODO: Decomplexify
472 protected virtual async ValueTask<TResult> RunRequest<TResult>(
473 string route,
474 HttpContent? content,
475 HttpMethod method,
476 long? instanceId,
477 bool tokenRefresh,
478 CancellationToken cancellationToken)
479 {
480 if (route == null)
481 throw new ArgumentNullException(nameof(route));
482 if (method == null)
483 throw new ArgumentNullException(nameof(method));
484 if (content == null && (method == HttpMethod.Post || method == HttpMethod.Put))
485 throw new InvalidOperationException("content cannot be null for POST or PUT!");
486
487 if (disposed)
488 throw new ObjectDisposedException(nameof(ApiClient));
489
490 HttpResponseMessage response;
491 var fullUri = new Uri(Url, route);
492 var serializerSettings = SerializerSettings;
493 var fileDownload = typeof(TResult) == typeof(Stream);
494 using (var request = new HttpRequestMessage(method, fullUri))
495 {
496 if (content != null)
497 request.Content = content;
498
499 try
500 {
501 var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
502 headersToUse.SetRequestHeaders(request.Headers, instanceId);
503
504 if (authless)
505 request.Headers.Remove(HeaderNames.Authorization);
506 else
507 {
508 var bearer = headersToUse.Token?.Bearer;
509 if (bearer != null)
510 {
511 var parsed = headersToUse.Token!.ParseJwt();
512 var nbf = parsed.ValidFrom;
513 var now = DateTime.UtcNow;
514 if (nbf >= now)
515 {
516 var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1));
517 await Task.Delay(delay, cancellationToken);
518 }
519 }
520 }
521
522 if (fileDownload)
523 request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Octet));
524
525 await ValueTaskExtensions.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
526
527 response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
528 }
529 finally
530 {
531 // prevent content param from getting disposed
532 request.Content = null;
533 }
534 }
535
536 try
537 {
538 await ValueTaskExtensions.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
539
540 // just stream
541 if (fileDownload && response.IsSuccessStatusCode)
542 return (TResult)(object)await CachedResponseStream.Create(response).ConfigureAwait(false);
543 }
544 catch
545 {
546 response.Dispose();
547 throw;
548 }
549
550 using (response)
551 {
552 var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
553
554 if (!response.IsSuccessStatusCode)
555 {
556 if (!tokenRefresh
557 && response.StatusCode == HttpStatusCode.Unauthorized
558 && await RefreshToken(cancellationToken).ConfigureAwait(false))
559 return await RunRequest<TResult>(route, content, method, instanceId, false, cancellationToken).ConfigureAwait(false);
560 HandleBadResponse(response, json);
561 }
562
563 if (String.IsNullOrWhiteSpace(json))
564 json = JsonConvert.SerializeObject(new object());
565
566 try
567 {
568 var result = JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
569 return result!;
570 }
571 catch (JsonException)
572 {
573 throw new UnrecognizedResponseException(response);
574 }
575 }
576 }
577#pragma warning restore CA1506
578
585 async ValueTask<HubConnection> WrapHubInitialConnectAuthRefresh(Func<ValueTask<HubConnection>> connectFunc, CancellationToken cancellationToken)
586 {
587 try
588 {
589 return await connectFunc();
590 }
591 catch (HttpRequestException ex)
592 {
593 // status code is not in netstandard
594 var propertyInfo = ex.GetType().GetProperty("StatusCode");
595 if (propertyInfo != null)
596 {
597 var statusCode = (HttpStatusCode)propertyInfo.GetValue(ex);
598 if (statusCode != HttpStatusCode.Unauthorized)
599 throw;
600 }
601
602 await RefreshToken(cancellationToken);
603
604 return await connectFunc();
605 }
606 }
607
620 async ValueTask<TResult> RunRequest<TBody, TResult>(
621 string route,
622 TBody? body,
623 HttpMethod method,
624 long? instanceId,
625 bool tokenRefresh,
626 CancellationToken cancellationToken)
627 where TBody : class
628 {
629 HttpContent? content = null;
630 if (body != null)
631 content = new StringContent(
632 JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, SerializerSettings),
633 Encoding.UTF8,
635
636 using (content)
637 return await RunRequest<TResult>(
638 route,
639 content,
640 method,
641 instanceId,
642 tokenRefresh,
643 cancellationToken)
644 .ConfigureAwait(false);
645 }
646
658 async ValueTask RunResultlessRequest<TBody>(
659 string route,
660 TBody? body,
661 HttpMethod method,
662 long? instanceId,
663 bool tokenRefresh,
664 CancellationToken cancellationToken)
665 where TBody : class
666 => await RunRequest<TBody, object>(
667 route,
668 body,
669 method,
670 instanceId,
671 tokenRefresh,
672 cancellationToken);
673
683 ValueTask RunRequest(
684 string route,
685 HttpMethod method,
686 long? instanceId,
687 bool tokenRefresh,
688 CancellationToken cancellationToken)
689 => RunResultlessRequest<object>(
690 route,
691 null,
692 method,
693 instanceId,
694 tokenRefresh,
695 cancellationToken);
696 }
697}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
ProductHeaderValue? UserAgent
The client's user agent as a ProductHeaderValue if valid.
Definition ApiHeaders.cs:89
void SetHubConnectionHeaders(IDictionary< string, string > headers)
Adds the headers necessary for a SignalR hub connection.
TokenResponse? Token
The client's TokenResponse.
void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers .
const string ApplicationJsonMime
Added to MediaTypeNames.Application in netstandard2.1. Can't use because of lack of ....
Definition ApiHeaders.cs:59
Represents an error message returned by the server.
Response for when file transfers are necessary.
Routes to a server actions.
Definition Routes.cs:9
const string ApiRoot
The root of API methods.
Definition Routes.cs:13
const string JobsHub
The root route of all hubs.
Definition Routes.cs:118
readonly? ApiHeaders tokenRefreshHeaders
Backing field for Headers.
Definition ApiClient.cs:89
TimeSpan Timeout
The request timeout.
Definition ApiClient.cs:54
static readonly JsonSerializerSettings SerializerSettings
The JsonSerializerSettings to use.
Definition ApiClient.cs:62
ValueTask< Stream > Download(FileTicketResponse ticket, CancellationToken cancellationToken)
Downloads a file Stream for a given ticket .A ValueTask<TResult> resulting in the downloaded Stream.
Definition ApiClient.cs:285
void AddRequestLogger(IRequestLogger requestLogger)
Adds a requestLogger to the request pipeline.
async ValueTask< TResult > RunRequest< TBody, TResult >(string route, TBody? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
Definition ApiClient.cs:620
readonly IHttpClient httpClient
The IHttpClient for the ApiClient.
Definition ApiClient.cs:74
async ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken)
Uploads a given uploadStream for a given ticket .A ValueTask representing the running operation.
Definition ApiClient.cs:300
ValueTask< TResult > Delete< TResult >(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask<TResult> resulting in the response body as a TResult .
Uri Url
The Uri pointing the tgstation-server.
Definition ApiClient.cs:43
ValueTask Delete(string route, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
ValueTask< TResult > Create< TBody, TResult >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP PUT request.A ValueTask<TResult> resulting in the response body as a TResult .
ApiHeaders headers
Backing field for Headers.
Definition ApiClient.cs:104
ValueTask< TResult > Update< TBody, TResult >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask Delete< TBody >(string route, TBody body, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
ValueTask< TResult > Read< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP GET request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask Patch(string route, CancellationToken cancellationToken)
Run an HTTP PATCH request.A ValueTask representing the running operation.
static void HandleBadResponse(HttpResponseMessage response, string json)
Handle a bad HTTP response .
Definition ApiClient.cs:116
static readonly HttpMethod HttpPatch
PATCH HttpMethod.
Definition ApiClient.cs:40
ValueTask< TResult > Delete< TBody, TResult >(string route, TBody body, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
virtual async ValueTask< TResult > RunRequest< TResult >(string route, HttpContent? content, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
Definition ApiClient.cs:472
readonly List< IRequestLogger > requestLoggers
The IRequestLoggers used by the ApiClient.
Definition ApiClient.cs:79
async ValueTask< IAsyncDisposable > CreateHubConnection< THubImplementation >(THubImplementation hubImplementation, IRetryPolicy? retryPolicy, Action< ILoggingBuilder >? loggingConfigureAction, CancellationToken cancellationToken)
Subscribe to all job updates available to the IRestServerClient.An IAsyncDisposable representing the ...
Definition ApiClient.cs:360
ValueTask< TResult > Create< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP PUT request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask< TResult > Update< TResult >(string route, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask<TResult> resulting in the response body as a TResult .
ValueTask< TResult > Patch< TResult >(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP PATCH request.A ValueTask<TResult> resulting in the response body as a TResult .
bool disposed
If the ApiClient is disposed.
Definition ApiClient.cs:109
async ValueTask< HubConnection > WrapHubInitialConnectAuthRefresh(Func< ValueTask< HubConnection > > connectFunc, CancellationToken cancellationToken)
Wrap a hub connection attempt via a connectFunc with proper token refreshing.
Definition ApiClient.cs:585
readonly bool authless
If the authentication header should be stripped from requests.
Definition ApiClient.cs:99
readonly SemaphoreSlim semaphoreSlim
The SemaphoreSlim for TokenResponse refreshes.
Definition ApiClient.cs:94
ValueTask RunRequest(string route, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
Main request method.
ValueTask Delete(string route, long instanceId, CancellationToken cancellationToken)
Run an HTTP DELETE request.A ValueTask representing the running operation.
readonly List< HubConnection > hubConnections
List of HubConnections created by the ApiClient.
Definition ApiClient.cs:84
async ValueTask< bool > RefreshToken(CancellationToken cancellationToken)
Attempt to refresh the stored Bearer token in Headers.
Definition ApiClient.cs:336
ApiHeaders Headers
The ApiHeaders the IApiClient uses.
Definition ApiClient.cs:47
ValueTask Update< TBody >(string route, TBody body, CancellationToken cancellationToken)
Run an HTTP POST request.A ValueTask representing the running operation.
ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, bool authless)
Initializes a new instance of the ApiClient class.
Definition ApiClient.cs:168
A IRetryPolicy that attempts to refresh a given apiClient's token on the first disconnect.
Occurs when the server returns a bad request response if the ApiException.ErrorCode is present....
Occurs when the client performs an action that would result in data conflict.
A IRetryPolicy that returns seconds in powers of 2, maxing out at 30s.
Occurs when the client attempts to perform an action they do not have the rights for.
Occurs when the client tries to use a currently unsupported API.
Occurs when a GitHub rate limit occurs.
Occurs when the client provides invalid credentials.
Occurs when an error occurs in the server.
Occurs when the client makes a request while the server is starting or stopping.
Occurs when the client provides invalid credentials.
Occurs when a response is received that did not deserialize to one of the expected Api....
Occurs when the API version of the client is not compatible with the server's.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
Caches the Stream from a HttpResponseMessage for later use.
static async ValueTask< CachedResponseStream > Create(HttpResponseMessage response)
Asyncronously creates a new CachedResponseStream.
Web interface for the API.
Definition IApiClient.cs:17
For logging HTTP requests and responses.
TimeSpan Timeout
The request timeout.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12