• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-dotnet / 11169244239

03 Oct 2024 08:28PM UTC coverage: 51.518% (+0.03%) from 51.493%
11169244239

Pull #2992

coveralls.net

web-flow
Merge 6cd055fd4 into ad875500a
Pull Request #2992: Remove special case Newtonsoft version, and update TargetFrameworks

5108 of 9915 relevant lines covered (51.52%)

107.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.54
/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs
1
namespace Stripe
2
{
3
    using System;
4
    using System.Collections.Generic;
5
    using System.Diagnostics;
6
    using System.IO;
7
    using System.Linq;
8
    using System.Net;
9
    using System.Net.Http;
10
    using System.Net.Http.Headers;
11
    using System.Threading;
12
    using System.Threading.Tasks;
13
    using Newtonsoft.Json;
14
    using Stripe.Infrastructure;
15

16
    /// <summary>
17
    /// Standard client to make requests to Stripe's API, using
18
    /// <see cref="System.Net.Http.HttpClient"/> to send HTTP requests. It can gather telemetry
19
    /// about request latency (via <see cref="RequestTelemetry"/>) and automatically retry failed
20
    /// requests when it's safe to do so.
21
    /// </summary>
22
    public class SystemNetHttpClient : IHttpClient
23
    {
24
        /// <summary>Default maximum number of retries made by the client.</summary>
25
        public const int DefaultMaxNumberRetries = 2;
26

27
        private const string StripeNetTargetFramework =
28
#if NET5_0
29
            "net5.0"
30
#elif NET6_0
31
            "net6.0"
32
#elif NET7_0
33
            "net7.0"
34
#elif NET8_0
35
            "net8.0"
36
#elif NETCOREAPP3_1
37
            "netcoreapp3.1"
38
#elif NETSTANDARD2_0
39
            "netstandard2.0"
40
#elif NET461
41
            "net461"
42
#else
43
#error "Unknown target framework"
44
#endif
45
            ;
46

47
        private static readonly Lazy<System.Net.Http.HttpClient> LazyDefaultHttpClient
1✔
48
            = new Lazy<System.Net.Http.HttpClient>(BuildDefaultSystemNetHttpClient);
1✔
49

50
        private readonly System.Net.Http.HttpClient httpClient;
51

52
        private readonly AppInfo appInfo;
53

54
        private readonly RequestTelemetry requestTelemetry = new RequestTelemetry();
1,575✔
55

56
        private readonly object randLock = new object();
1,575✔
57

58
        private readonly Random rand = new Random();
1,575✔
59

60
        private string stripeClientUserAgentString;
61

62
        static SystemNetHttpClient()
63
        {
1✔
64
            // Enable support for TLS 1.2, as Stripe's API requires it. This should only be
65
            // necessary for .NET Framework 4.5 as more recent runtimes should have TLS 1.2 enabled
66
            // by default, but it can be disabled in some environments.
67
            ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol |
1✔
68
                SecurityProtocolType.Tls12;
1✔
69
        }
1✔
70

71
        /// <summary>
72
        /// Initializes a new instance of the <see cref="SystemNetHttpClient"/> class.
73
        /// </summary>
74
        /// <param name="httpClient">
75
        /// The <see cref="System.Net.Http.HttpClient"/> client to use. If <c>null</c>, an HTTP
76
        /// client will be created with default parameters.
77
        /// </param>
78
        /// <param name="maxNetworkRetries">
79
        /// The maximum number of times the client will retry requests that fail due to an
80
        /// intermittent problem.
81
        /// </param>
82
        /// <param name="appInfo">
83
        /// Information about the "app" which this integration belongs to. This should be reserved
84
        /// for plugins that wish to identify themselves with Stripe.
85
        /// </param>
86
        /// <param name="enableTelemetry">
87
        /// Whether to enable request latency telemetry or not.
88
        /// </param>
89
        public SystemNetHttpClient(
1,575✔
90
            System.Net.Http.HttpClient httpClient = null,
1,575✔
91
            int maxNetworkRetries = DefaultMaxNumberRetries,
1,575✔
92
            AppInfo appInfo = null,
1,575✔
93
            bool enableTelemetry = true)
1,575✔
94
        {
1,575✔
95
            this.httpClient = httpClient ?? LazyDefaultHttpClient.Value;
1,575✔
96
            this.MaxNetworkRetries = maxNetworkRetries;
1,575✔
97
            this.appInfo = appInfo;
1,575✔
98
            this.EnableTelemetry = enableTelemetry;
1,575✔
99

100
            this.stripeClientUserAgentString = this.BuildStripeClientUserAgentString();
1,575✔
101
        }
1,575✔
102

103
        /// <summary>Default timespan before the request times out.</summary>
104
        public static TimeSpan DefaultHttpTimeout => TimeSpan.FromSeconds(80);
1✔
105

106
        /// <summary>
107
        /// Maximum sleep time between tries to send HTTP requests after network failure.
108
        /// </summary>
109
        public static TimeSpan MaxNetworkRetriesDelay => TimeSpan.FromSeconds(5);
×
110

111
        /// <summary>
112
        /// Minimum sleep time between tries to send HTTP requests after network failure.
113
        /// </summary>
114
        public static TimeSpan MinNetworkRetriesDelay => TimeSpan.FromMilliseconds(500);
×
115

116
        /// <summary>
117
        /// Gets whether telemetry was enabled for this client.
118
        /// </summary>
119
        public bool EnableTelemetry { get; }
120

121
        /// <summary>
122
        /// Gets how many network retries were configured for this client.
123
        /// </summary>
124
        public int MaxNetworkRetries { get; }
125

126
        /// <summary>
127
        /// Gets or sets a value indicating whether the client should sleep between automatic
128
        /// request retries.
129
        /// </summary>
130
        /// <remarks>This is an internal property meant to be used in tests only.</remarks>
131
        internal bool NetworkRetriesSleep { get; set; } = true;
1,575✔
132

133
        /// <summary>
134
        /// Initializes a new instance of the <see cref="System.Net.Http.HttpClient"/> class
135
        /// with default parameters.
136
        /// </summary>
137
        /// <returns>The new instance of the <see cref="System.Net.Http.HttpClient"/> class.</returns>
138
        public static System.Net.Http.HttpClient BuildDefaultSystemNetHttpClient()
139
        {
1✔
140
            // We set the User-Agent and X-Stripe-Client-User-Agent headers in each request
141
            // message rather than through the client's `DefaultRequestHeaders` because we
142
            // want these headers to be present even when a custom HTTP client is used.
143
            return new System.Net.Http.HttpClient
1✔
144
            {
1✔
145
                Timeout = DefaultHttpTimeout,
1✔
146
            };
1✔
147
        }
1✔
148

149
        /// <summary>Sends a request to Stripe's API as an asynchronous operation.</summary>
150
        /// <param name="request">The parameters of the request to send.</param>
151
        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
152
        /// <returns>The task object representing the asynchronous operation.</returns>
153
        public async Task<StripeResponse> MakeRequestAsync(
154
            StripeRequest request,
155
            CancellationToken cancellationToken = default)
156
        {
157
            var (response, retries) = await this.SendHttpRequest(request, cancellationToken).ConfigureAwait(false);
158

159
            var reader = new StreamReader(
160
                await response.Content.ReadAsStreamAsync().ConfigureAwait(false));
161

162
            return new StripeResponse(
163
                response.StatusCode,
164
                response.Headers,
165
                await reader.ReadToEndAsync().ConfigureAwait(false))
166
            {
167
                NumRetries = retries,
168
            };
169
        }
170

171
        public async Task<StripeStreamedResponse> MakeStreamingRequestAsync(
172
            StripeRequest request,
173
            CancellationToken cancellationToken = default)
174
        {
175
            var (response, retries) = await this.SendHttpRequest(request, cancellationToken).ConfigureAwait(false);
176

177
            return new StripeStreamedResponse(
178
                response.StatusCode,
179
                response.Headers,
180
                await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
181
            {
182
                NumRetries = retries,
183
            };
184
        }
185

186
        private async Task<(HttpResponseMessage responseMessage, int retries)> SendHttpRequest(
187
            StripeRequest request,
188
            CancellationToken cancellationToken)
189
        {
190
            TimeSpan duration;
191
            Exception requestException;
192
            HttpResponseMessage response = null;
193
            int retry = 0;
194

195
            if (this.EnableTelemetry)
196
            {
197
                this.requestTelemetry.MaybeAddTelemetryHeader(request.StripeHeaders);
198
            }
199

200
            while (true)
201
            {
202
                requestException = null;
203

204
                var httpRequest = this.BuildRequestMessage(request);
205

206
                var stopwatch = Stopwatch.StartNew();
207

208
                try
209
                {
210
                    response = await this.httpClient.SendAsync(httpRequest, cancellationToken)
211
                        .ConfigureAwait(false);
212
                }
213
                catch (HttpRequestException exception)
214
                {
215
                    requestException = exception;
216
                }
217
                catch (OperationCanceledException exception)
218
                    when (!cancellationToken.IsCancellationRequested)
219
                {
220
                    requestException = exception;
221
                }
222

223
                stopwatch.Stop();
224

225
                duration = stopwatch.Elapsed;
226

227
                if (!this.ShouldRetry(
228
                    retry,
229
                    requestException != null,
230
                    response?.StatusCode,
231
                    response?.Headers))
232
                {
233
                    break;
234
                }
235

236
                retry += 1;
237
                await Task.Delay(this.SleepTime(retry)).ConfigureAwait(false);
238
            }
239

240
            if (requestException != null)
241
            {
242
                throw requestException;
243
            }
244

245
            if (this.EnableTelemetry)
246
            {
247
                this.requestTelemetry.MaybeEnqueueMetrics(response, duration, request.Usage);
248
            }
249

250
            return (response, retry);
251
        }
252

253
        private string BuildStripeClientUserAgentString()
254
        {
1,575✔
255
            var values = new Dictionary<string, object>
1,575✔
256
            {
1,575✔
257
                { "bindings_version", StripeConfiguration.StripeNetVersion },
1,575✔
258
                { "lang", ".net" },
1,575✔
259
                { "publisher", "stripe" },
1,575✔
260
                { "stripe_net_target_framework", StripeNetTargetFramework },
1,575✔
261
            };
1,575✔
262

263
            // The following values are in try/catch blocks on the off chance that the
264
            // RuntimeInformation methods fail in an unexpected way. This should ~never happen, but
265
            // if it does it should not prevent users from sending requests.
266
            // See https://github.com/stripe/stripe-dotnet/issues/1986 for context.
267
            try
268
            {
1,575✔
269
                values.Add("lang_version", RuntimeInformation.GetRuntimeVersion());
1,575✔
270
            }
1,575✔
271
            catch (Exception)
×
272
            {
×
273
                values.Add("lang_version", "(unknown)");
×
274
            }
×
275

276
            try
277
            {
1,575✔
278
                values.Add("os_version", RuntimeInformation.GetOSVersion());
1,575✔
279
            }
1,575✔
280
            catch (Exception)
×
281
            {
×
282
                values.Add("os_version", "(unknown)");
×
283
            }
×
284

285
            try
286
            {
1,575✔
287
                values.Add("newtonsoft_json_version", RuntimeInformation.GetNewtonsoftJsonVersion());
1,575✔
288
            }
1,575✔
289
            catch (Exception)
×
290
            {
×
291
                values.Add("newtonsoft_json_version", "(unknown)");
×
292
            }
×
293

294
            if (this.appInfo != null)
1,575✔
295
            {
1✔
296
                values.Add("application", this.appInfo);
1✔
297
            }
1✔
298

299
            return JsonUtils.SerializeObject(values, Formatting.None);
1,575✔
300
        }
1,575✔
301

302
        private string BuildUserAgentString(ApiMode apiMode)
303
        {
1,264✔
304
            var userAgent = $"Stripe/{apiMode} .NetBindings/{StripeConfiguration.StripeNetVersion}";
1,264✔
305

306
            if (this.appInfo != null)
1,264✔
307
            {
1✔
308
                userAgent += " " + this.appInfo.FormatUserAgent();
1✔
309
            }
1✔
310

311
            return userAgent;
1,264✔
312
        }
1,264✔
313

314
        private bool ShouldRetry(
315
            int numRetries,
316
            bool error,
317
            HttpStatusCode? statusCode,
318
            HttpHeaders headers)
319
        {
1,263✔
320
            // Do not retry if we are out of retries.
321
            if (numRetries >= this.MaxNetworkRetries)
1,263✔
322
            {
2✔
323
                return false;
2✔
324
            }
325

326
            // Retry on connection error.
327
            if (error == true)
1,261✔
328
            {
6✔
329
                return true;
6✔
330
            }
331

332
            // The API may ask us not to retry (eg; if doing so would be a no-op)
333
            // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
334
            if (headers != null && headers.Contains("Stripe-Should-Retry"))
1,255✔
335
            {
×
336
                var value = headers.GetValues("Stripe-Should-Retry").First();
×
337

338
                switch (value)
×
339
                {
340
                    case "true":
341
                        return true;
×
342
                    case "false":
343
                        return false;
×
344
                }
345
            }
×
346

347
            // Retry on conflict errors.
348
            if (statusCode == HttpStatusCode.Conflict)
1,255✔
349
            {
1✔
350
                return true;
1✔
351
            }
352

353
            // Retry on 500, 503, and other internal errors.
354
            //
355
            // Note that we expect the Stripe-Should-Retry header to be false
356
            // in most cases when a 500 is returned, since our idempotency framework
357
            // would typically replay it anyway.
358
            if (statusCode.HasValue && ((int)statusCode.Value >= 500))
1,254✔
359
            {
2✔
360
                return true;
2✔
361
            }
362

363
            return false;
1,252✔
364
        }
1,263✔
365

366
        private System.Net.Http.HttpRequestMessage BuildRequestMessage(StripeRequest request)
367
        {
1,264✔
368
            var requestMessage = new System.Net.Http.HttpRequestMessage(request.Method, request.Uri);
1,264✔
369

370
            // Standard headers
371
            requestMessage.Headers.TryAddWithoutValidation("User-Agent", this.BuildUserAgentString(request.ApiMode));
1,264✔
372
            requestMessage.Headers.Authorization = request.AuthorizationHeader;
1,264✔
373

374
            // Custom headers
375
            requestMessage.Headers.Add("X-Stripe-Client-User-Agent", this.stripeClientUserAgentString);
1,264✔
376
            foreach (var header in request.StripeHeaders)
7,326✔
377
            {
1,767✔
378
                requestMessage.Headers.Add(header.Key, header.Value);
1,767✔
379
            }
1,767✔
380

381
            // Request body
382
            requestMessage.Content = request.Content;
1,264✔
383

384
            return requestMessage;
1,264✔
385
        }
1,264✔
386

387
        private TimeSpan SleepTime(int numRetries)
388
        {
9✔
389
            // We disable sleeping in some cases for tests.
390
            if (!this.NetworkRetriesSleep)
9✔
391
            {
9✔
392
                return TimeSpan.Zero;
9✔
393
            }
394

395
            // Apply exponential backoff with MinNetworkRetriesDelay on the number of numRetries
396
            // so far as inputs.
397
            var delay = TimeSpan.FromTicks((long)(MinNetworkRetriesDelay.Ticks
×
398
                * Math.Pow(2, numRetries - 1)));
×
399

400
            // Do not allow the number to exceed MaxNetworkRetriesDelay
401
            if (delay > MaxNetworkRetriesDelay)
×
402
            {
×
403
                delay = MaxNetworkRetriesDelay;
×
404
            }
×
405

406
            // Apply some jitter by randomizing the value in the range of 75%-100%.
407
            var jitter = 1.0;
×
408
            lock (this.randLock)
×
409
            {
×
410
                jitter = (3.0 + this.rand.NextDouble()) / 4.0;
×
411
            }
×
412

413
            delay = TimeSpan.FromTicks((long)(delay.Ticks * jitter));
×
414

415
            // But never sleep less than the base sleep seconds.
416
            if (delay < MinNetworkRetriesDelay)
×
417
            {
×
418
                delay = MinNetworkRetriesDelay;
×
419
            }
×
420

421
            return delay;
×
422
        }
9✔
423
    }
424
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc