|
1 | 1 | using System.IO.Compression; |
2 | 2 | using System.Net; |
3 | 3 | using System.Net.Http.Headers; |
| 4 | +using System.Net.Sockets; |
4 | 5 | using System.Text; |
5 | 6 | using System.Text.Json; |
6 | 7 | using Microsoft.Extensions.Time.Testing; |
@@ -549,6 +550,227 @@ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => |
549 | 550 | #endif |
550 | 551 | } |
551 | 552 |
|
| 553 | +public class ThePostJsonWithNetworkRetryAsyncMethod |
| 554 | +{ |
| 555 | + static readonly Uri FlagsUrl = new("https://us.i.posthog.com/flags/?v=2"); |
| 556 | + |
| 557 | + static PostHogOptions CreateOptions(int maxRetries = 3) => new() |
| 558 | + { |
| 559 | + ProjectToken = "test-api-key", |
| 560 | + MaxRetries = maxRetries, |
| 561 | + FeatureFlagRequestMaxRetries = maxRetries, |
| 562 | + InitialRetryDelay = TimeSpan.FromMilliseconds(1), |
| 563 | + MaxRetryDelay = TimeSpan.FromSeconds(30) |
| 564 | + }; |
| 565 | + |
| 566 | + static HttpClient CreateHttpClient(FakeRetryHttpMessageHandler handler) |
| 567 | + => new(handler) { BaseAddress = new Uri("https://us.i.posthog.com") }; |
| 568 | + |
| 569 | + [Fact] |
| 570 | + public async Task RetriesOnConnectionResetThenSucceeds() |
| 571 | + { |
| 572 | + var handler = new FakeRetryHttpMessageHandler(); |
| 573 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 574 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 575 | + using var httpClient = CreateHttpClient(handler); |
| 576 | + var options = CreateOptions(); |
| 577 | + var timeProvider = new FakeTimeProvider(); |
| 578 | + |
| 579 | + var task = httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 580 | + FlagsUrl, |
| 581 | + new { api_key = "test", distinct_id = "user-1" }, |
| 582 | + timeProvider, |
| 583 | + options, |
| 584 | + CancellationToken.None); |
| 585 | + |
| 586 | + await handler.WaitForRequestCountAsync(1); |
| 587 | + timeProvider.Advance(TimeSpan.FromSeconds(1)); |
| 588 | + var result = await task; |
| 589 | + |
| 590 | + Assert.NotNull(result); |
| 591 | + Assert.Equal(2, handler.RequestCount); |
| 592 | + } |
| 593 | + |
| 594 | + [Fact] |
| 595 | + public async Task RetriesUntilSuccessAfterMultipleConnectionResetErrors() |
| 596 | + { |
| 597 | + var handler = new FakeRetryHttpMessageHandler(); |
| 598 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 599 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 600 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 601 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 602 | + using var httpClient = CreateHttpClient(handler); |
| 603 | + var options = CreateOptions(maxRetries: 3); |
| 604 | + var timeProvider = new FakeTimeProvider(); |
| 605 | + |
| 606 | + var task = httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 607 | + FlagsUrl, |
| 608 | + new { api_key = "test", distinct_id = "user-1" }, |
| 609 | + timeProvider, |
| 610 | + options, |
| 611 | + CancellationToken.None); |
| 612 | + |
| 613 | + for (var i = 1; i <= 4 && !task.IsCompleted; i++) |
| 614 | + { |
| 615 | + await handler.WaitForRequestCountAsync(i); |
| 616 | + timeProvider.Advance(TimeSpan.FromMilliseconds(50)); |
| 617 | + } |
| 618 | + |
| 619 | + var result = await task; |
| 620 | + |
| 621 | + Assert.NotNull(result); |
| 622 | + Assert.Equal(4, handler.RequestCount); |
| 623 | + } |
| 624 | + |
| 625 | + [Fact] |
| 626 | + public async Task ThrowsAfterMaxRetriesWhenConnectionResetPersists() |
| 627 | + { |
| 628 | + var handler = new FakeRetryHttpMessageHandler(); |
| 629 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 630 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 631 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 632 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 633 | + using var httpClient = CreateHttpClient(handler); |
| 634 | + var options = CreateOptions(maxRetries: 3); |
| 635 | + var timeProvider = new FakeTimeProvider(); |
| 636 | + |
| 637 | + var task = httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 638 | + FlagsUrl, |
| 639 | + new { api_key = "test", distinct_id = "user-1" }, |
| 640 | + timeProvider, |
| 641 | + options, |
| 642 | + CancellationToken.None); |
| 643 | + |
| 644 | + for (var i = 1; i <= 4 && !task.IsCompleted; i++) |
| 645 | + { |
| 646 | + await handler.WaitForRequestCountAsync(i); |
| 647 | + timeProvider.Advance(TimeSpan.FromMilliseconds(50)); |
| 648 | + } |
| 649 | + |
| 650 | + await Assert.ThrowsAsync<HttpRequestException>(() => task); |
| 651 | + Assert.Equal(4, handler.RequestCount); |
| 652 | + } |
| 653 | + |
| 654 | + [Fact] |
| 655 | + public async Task DoesNotRetryWhenFeatureFlagRequestMaxRetriesIsZero() |
| 656 | + { |
| 657 | + var handler = new FakeRetryHttpMessageHandler(); |
| 658 | + handler.AddException(new HttpRequestException("Connection reset", new SocketException((int)SocketError.ConnectionReset))); |
| 659 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 660 | + using var httpClient = CreateHttpClient(handler); |
| 661 | + var options = CreateOptions(maxRetries: 0); |
| 662 | + var timeProvider = new FakeTimeProvider(); |
| 663 | + |
| 664 | + await Assert.ThrowsAsync<HttpRequestException>(() => |
| 665 | + httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 666 | + FlagsUrl, |
| 667 | + new { api_key = "test", distinct_id = "user-1" }, |
| 668 | + timeProvider, |
| 669 | + options, |
| 670 | + CancellationToken.None)); |
| 671 | + |
| 672 | + Assert.Equal(1, handler.RequestCount); |
| 673 | + } |
| 674 | + |
| 675 | +#if NET8_0_OR_GREATER |
| 676 | + [Fact] |
| 677 | + public async Task DoesNotRetryOnUserCancellation() |
| 678 | + { |
| 679 | + var handler = new FakeRetryHttpMessageHandler(); |
| 680 | + using var cts = new CancellationTokenSource(); |
| 681 | + await cts.CancelAsync(); |
| 682 | + handler.AddException(new TaskCanceledException("Operation was canceled.", null, cts.Token)); |
| 683 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 684 | + using var httpClient = CreateHttpClient(handler); |
| 685 | + var options = CreateOptions(); |
| 686 | + var timeProvider = new FakeTimeProvider(); |
| 687 | + |
| 688 | + await Assert.ThrowsAnyAsync<OperationCanceledException>(() => |
| 689 | + httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 690 | + FlagsUrl, |
| 691 | + new { api_key = "test", distinct_id = "user-1" }, |
| 692 | + timeProvider, |
| 693 | + options, |
| 694 | + cts.Token)); |
| 695 | + |
| 696 | + Assert.Equal(1, handler.RequestCount); |
| 697 | + } |
| 698 | +#endif |
| 699 | + |
| 700 | + [Fact] |
| 701 | + public async Task DoesNotRetryConnectionRefused() |
| 702 | + { |
| 703 | + var handler = new FakeRetryHttpMessageHandler(); |
| 704 | + handler.AddException(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused))); |
| 705 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 706 | + using var httpClient = CreateHttpClient(handler); |
| 707 | + var options = CreateOptions(); |
| 708 | + var timeProvider = new FakeTimeProvider(); |
| 709 | + |
| 710 | + await Assert.ThrowsAsync<HttpRequestException>(() => |
| 711 | + httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 712 | + FlagsUrl, |
| 713 | + new { api_key = "test", distinct_id = "user-1" }, |
| 714 | + timeProvider, |
| 715 | + options, |
| 716 | + CancellationToken.None)); |
| 717 | + |
| 718 | + Assert.Equal(1, handler.RequestCount); |
| 719 | + } |
| 720 | + |
| 721 | + [Fact] |
| 722 | + public async Task RetriesOnTaskCanceledExceptionFromTimeoutThenSucceeds() |
| 723 | + { |
| 724 | + var handler = new FakeRetryHttpMessageHandler(); |
| 725 | + handler.AddException(new TaskCanceledException("The request timed out.")); |
| 726 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); |
| 727 | + using var httpClient = CreateHttpClient(handler); |
| 728 | + var options = CreateOptions(); |
| 729 | + var timeProvider = new FakeTimeProvider(); |
| 730 | + |
| 731 | + var task = httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 732 | + FlagsUrl, |
| 733 | + new { api_key = "test", distinct_id = "user-1" }, |
| 734 | + timeProvider, |
| 735 | + options, |
| 736 | + CancellationToken.None); |
| 737 | + |
| 738 | + await handler.WaitForRequestCountAsync(1); |
| 739 | + timeProvider.Advance(TimeSpan.FromSeconds(1)); |
| 740 | + var result = await task; |
| 741 | + |
| 742 | + Assert.NotNull(result); |
| 743 | + Assert.Equal(2, handler.RequestCount); |
| 744 | + } |
| 745 | + |
| 746 | + [Theory] |
| 747 | + [InlineData(HttpStatusCode.RequestTimeout)] // 408 |
| 748 | + [InlineData(HttpStatusCode.TooManyRequests)] // 429 |
| 749 | + [InlineData(HttpStatusCode.InternalServerError)] // 500 |
| 750 | + [InlineData(HttpStatusCode.BadGateway)] // 502 |
| 751 | + [InlineData(HttpStatusCode.ServiceUnavailable)] // 503 |
| 752 | + [InlineData(HttpStatusCode.GatewayTimeout)] // 504 |
| 753 | + public async Task DoesNotRetryOnHttpErrorStatusCodes(HttpStatusCode statusCode) |
| 754 | + { |
| 755 | + var handler = new FakeRetryHttpMessageHandler(); |
| 756 | + handler.AddResponse(statusCode, new { type = "error", detail = "server error" }); |
| 757 | + handler.AddResponse(HttpStatusCode.OK, new { flags = new { } }); // Should never be reached |
| 758 | + using var httpClient = CreateHttpClient(handler); |
| 759 | + var options = CreateOptions(); |
| 760 | + var timeProvider = new FakeTimeProvider(); |
| 761 | + |
| 762 | + await Assert.ThrowsAsync<ApiException>(() => |
| 763 | + httpClient.PostJsonWithNetworkRetryAsync<FlagsApiResult>( |
| 764 | + FlagsUrl, |
| 765 | + new { api_key = "test", distinct_id = "user-1" }, |
| 766 | + timeProvider, |
| 767 | + options, |
| 768 | + CancellationToken.None)); |
| 769 | + |
| 770 | + Assert.Equal(1, handler.RequestCount); |
| 771 | + } |
| 772 | +} |
| 773 | + |
552 | 774 | public class ThePostCompressedJsonAsyncMethod |
553 | 775 | { |
554 | 776 | static readonly Uri BatchUrl = new("https://us.i.posthog.com/batch"); |
|
0 commit comments