forked from KoenZomers/TadoApi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttp.cs
More file actions
329 lines (291 loc) · 16 KB
/
Copy pathHttp.cs
File metadata and controls
329 lines (291 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
using KoenZomers.Tado.Api.Helpers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net;
using System.Text;
using System.Text.Json;
namespace KoenZomers.Tado.Api.Controllers;
/// <summary>
/// Controller wich allows to perform HTTP calls
/// </summary>
public class Http : Base
{
#region Properties
/// <summary>
/// Default timeout for HTTP requests in seconds
/// </summary>
private short DefaultApiTimeoutSeconds => _configuration?.CurrentValue.DefaultApiTimeoutSeconds ?? 30;
#endregion
#region Fields
/// <summary>
/// HttpClient to use for network communications towards the Tado API
/// </summary>
private readonly HttpClient? _tadoHttpClient;
/// <summary>
/// Access to the application configuration
/// </summary>
private readonly IOptionsMonitor<Configuration.Tado>? _configuration;
#endregion
/// <summary>
/// Instantiate the HTTP controller
/// </summary>
/// <param name="httpClientFactory">HttpClientFactory to use to retrieve a HttpClient from</param>
/// <param name="configuration">The application configuration</param>
/// <param name="loggerFactory">LoggerFactory to use to retrieve Logger instance from</param>
public Http(IHttpClientFactory httpClientFactory, IOptionsMonitor<Configuration.Tado> configuration, ILoggerFactory loggerFactory) : base(loggerFactory: loggerFactory)
{
_configuration = configuration;
_tadoHttpClient = httpClientFactory.CreateClient("Tado");
_tadoHttpClient.Timeout = TimeSpan.FromSeconds(DefaultApiTimeoutSeconds);
}
/// <summary>
/// Sends a HTTP POST to the provided uri
/// </summary>
/// <param name="queryBuilder">The querystring parameters to send in the POST body</param>
/// <typeparam name="T">Type of object to try to parse the response JSON into</typeparam>
/// <param name="uri">Uri of the webservice to send the message to</param>
/// <param name="cancellationToken">Cancellation token will be used to cancel the request and exit the retry loop.</param>
/// <param name="retryIntervalIfFailed">If provided, in case of a non 2xx response, it will keep retrying the call. Optional, if not provided, it will not retry and throw a <see cref="Exceptions.RequestFailedException"/> Exception if it fails.</param>
/// <param name="maximumRetries">If provided, in case of a non 2xx response, it will retry the call at most the amount configured through this parameter. Optional, if not provided, it will endlessly retry. If <paramref name="retryIntervalIfFailed"/> has not been set, this is being ignored.</param>
/// <param name="token">Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously.</param>
/// <returns>Object of type T with the parsed response</returns>
/// <exception cref="Exceptions.RequestThrottledException">Thrown when the request is getting throttled</exception>
/// <exception cref="Exceptions.RequestFailedException">Thrown when the request has failed.</exception>
public async Task<T?> PostMessageGetResponse<T>(Uri uri, QueryStringBuilder queryBuilder, CancellationToken cancellationToken, short? retryIntervalIfFailed = null, short? maximumRetries = null, Models.Authentication.Token? token = null)
{
ArgumentNullException.ThrowIfNull(uri);
ArgumentNullException.ThrowIfNull(_tadoHttpClient);
var retryCount = 0;
do
{
if (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug("Cancellation requested, stopping retries for POST to {Uri}", uri);
return default;
}
// Request the response from the webservice
Logger.LogDebug("Calling Tado API at {Uri} with content: {Content}", uri, queryBuilder.ToString());
// Prepare the content to POST
using var content = new StringContent(queryBuilder.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded");
// Construct the message towards the webservice
using var request = new HttpRequestMessage(HttpMethod.Post, uri);
// Check if we should include an Authorization Bearer token
if (token is not null)
{
request.Headers.Authorization = new("Bearer", token.AccessToken);
}
// Set the content to send along in the message body with the request
request.Content = content;
retryCount++;
HttpResponseMessage response;
Stream responseContentStream;
try
{
response = await _tadoHttpClient.SendAsync(request, cancellationToken);
responseContentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(PostMessageGetResponse), uri);
return default;
}
catch (Exception ex)
{
throw new Exceptions.RequestFailedException(uri, ex);
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Request was throttled
throw new Exceptions.RequestThrottledException(uri, response);
}
if (!response.IsSuccessStatusCode)
{
// Request was not successful
if (!retryIntervalIfFailed.HasValue || retryCount >= maximumRetries)
{
// We should not retry or we have reached the maximum number of retries
throw new Exceptions.RequestFailedException(uri);
}
// Pause and retry
Logger.LogDebug($"Request failed with status code {response.StatusCode} for URI {uri}. Retrying in {retryIntervalIfFailed.Value} seconds...");
await Task.Delay(TimeSpan.FromSeconds(retryIntervalIfFailed.Value), cancellationToken);
}
else
{
// Request was successful (response status 200-299)
var responseEntity = await JsonSerializer.DeserializeAsync<T>(responseContentStream, cancellationToken: CancellationToken.None);
return responseEntity;
}
} while (true);
}
/// <summary>
/// Sends a message to the Tado API and returns the provided object of type T with the response
/// </summary>
/// <typeparam name="T">Object type of the expected response</typeparam>
/// <param name="uri">Uri of the webservice to send the message to</param>
/// <param name="cancellationToken">Cancellation token will be used to cancel the request.</param>
/// <param name="expectedHttpStatusCode">The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure.</param>
/// <param name="token">Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously.</param>
/// <returns>Typed entity with the result from the webservice</returns>
/// <exception cref="Exceptions.RequestThrottledException">Thrown when the request is getting throttled</exception>
/// <exception cref="Exceptions.RequestFailedException">Thrown when the request has failed.</exception>
public async Task<T?> GetMessageReturnResponse<T>(Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null)
{
ArgumentNullException.ThrowIfNull(uri);
ArgumentNullException.ThrowIfNull(_tadoHttpClient);
// Construct the request towards the webservice
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
// Check if we should include an Authorization Bearer token
if (token is not null)
{
request.Headers.Authorization = new("Bearer", token.AccessToken);
}
HttpResponseMessage response;
try
{
// Request the response from the webservice
response = await _tadoHttpClient.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(GetMessageReturnResponse), uri);
return default;
}
catch (Exception ex)
{
// Request was not successful, throw an exception
throw new Exceptions.RequestFailedException(uri, ex);
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Request was throttled
throw new Exceptions.RequestThrottledException(uri, response);
}
if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value)
{
var responseContentStream = await response.Content.ReadAsStreamAsync(CancellationToken.None);
var responseEntity = await JsonSerializer.DeserializeAsync<T>(responseContentStream, cancellationToken: CancellationToken.None);
return responseEntity;
}
return default;
}
/// <summary>
/// Sends a message to the Tado API and returns the provided object of type T with the response
/// </summary>
/// <typeparam name="T">Object type of the expected response</typeparam>
/// <param name="uri">Uri of the webservice to send the message to</param>
/// <param name="bodyText">Text to send to the webservice in the body</param>
/// <param name="httpMethod">Http Method to use to connect to the webservice</param>
/// <param name="cancellationToken">Cancellation token to cancel the request</param>
/// <param name="expectedHttpStatusCode">The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure.</param>
/// <param name="token">Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously.</param>
/// <returns>Typed entity with the result from the webservice</returns>
/// <exception cref="Exceptions.RequestThrottledException">Thrown when the request is getting throttled</exception>
/// <exception cref="Exceptions.RequestFailedException">Thrown when the request has failed.</exception>
public async Task<T?> SendMessageReturnResponse<T>(string bodyText, HttpMethod httpMethod, Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null)
{
ArgumentNullException.ThrowIfNull(uri);
ArgumentNullException.ThrowIfNull(_tadoHttpClient);
// Load the content to send in the body
using var content = new StringContent(bodyText ?? "", Encoding.UTF8, "application/json");
// Construct the message towards the webservice
using var request = new HttpRequestMessage(httpMethod, uri);
// Check if we should include an Authorization Bearer token
if (token is not null)
{
request.Headers.Authorization = new("Bearer", token.AccessToken);
}
// Check if a body to send along with the request has been provided
if (!string.IsNullOrEmpty(bodyText) && httpMethod != HttpMethod.Get)
{
// Set the content to send along in the message body with the request
request.Content = content;
}
HttpResponseMessage response;
try
{
// Request the response from the webservice
response = await _tadoHttpClient.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(SendMessageReturnResponse), uri);
return default;
}
catch (Exception ex)
{
// Request was not successful. throw an exception
throw new Exceptions.RequestFailedException(uri, ex);
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Request was throttled
throw new Exceptions.RequestThrottledException(uri, response);
}
if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value)
{
var responseContentStream = await response.Content.ReadAsStreamAsync(CancellationToken.None);
var responseEntity = await JsonSerializer.DeserializeAsync<T>(responseContentStream, cancellationToken: CancellationToken.None);
return responseEntity;
}
return default;
}
/// <summary>
/// Sends a message to the Tado API without looking at the response
/// </summary>
/// <param name="uri">Uri of the webservice to send the message to</param>
/// <param name="bodyText">Text to send to the webservice in the body</param>
/// <param name="httpMethod">Http Method to use to connect to the webservice</param>
/// <param name="cancellationToken">Cancellation token to cancel the request</param>
/// <param name="expectedHttpStatusCode">The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be false to indicate failure.</param>
/// <param name="token">Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously.</param>
/// <returns>Boolean indicating if the request was successful</returns>
/// <exception cref="Exceptions.RequestThrottledException">Thrown when the request is getting throttled</exception>
/// <exception cref="Exceptions.RequestFailedException">Thrown when the request has failed.</exception>
public async Task<bool> SendMessage(string bodyText, HttpMethod httpMethod, Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null)
{
ArgumentNullException.ThrowIfNull(uri);
ArgumentNullException.ThrowIfNull(_tadoHttpClient);
// Load the content to send in the body
using var content = new StringContent(bodyText ?? "", Encoding.UTF8, "application/json");
// Construct the message towards the webservice
using var request = new HttpRequestMessage(httpMethod, uri);
// Check if we should include an Authorization Bearer token
if (token is not null)
{
request.Headers.Authorization = new("Bearer", token.AccessToken);
}
// Check if a body to send along with the request has been provided
if (!string.IsNullOrEmpty(bodyText) && httpMethod != HttpMethod.Get)
{
// Set the content to send along in the message body with the request
request.Content = content;
}
HttpResponseMessage response;
try
{
// Request the response from the webservice
response = await _tadoHttpClient.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(SendMessage), uri);
return false;
}
catch (Exception ex)
{
// Request was not successful. throw an exception
throw new Exceptions.RequestFailedException(uri, ex);
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Request was throttled
throw new Exceptions.RequestThrottledException(uri, response);
}
if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value)
{
return true;
}
return false;
}
}