Skip to content

Commit 2942ea9

Browse files
committed
Merge PR #1478: Fix unnecessary retries on repository existence checks
2 parents 7d5682e + abd14ab commit 2942ea9

File tree

3 files changed

+137
-7
lines changed

3 files changed

+137
-7
lines changed

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
- Added validation to detect and return clear error messages when a URL is provided instead of a name for organization, repository, or enterprise arguments (e.g., `--github-org`, `--github-target-org`, `--source-repo`, `--github-target-enterprise`)
2+
- Fixed an issue where repository existence checks would incorrectly retry on expected responses (200/404/301), causing unnecessary delays during migrations. The check now only retries on transient server errors (5xx status codes) while responding immediately to deterministic states.
23
- Added `--target-api-url` as an optional arg to the `add-team-to-repo` command

src/Octoshift/Services/GithubClient.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ public GithubClient(OctoLogger log, HttpClient httpClient, IVersionProvider vers
4545
}
4646
}
4747

48-
public virtual async Task<string> GetNonSuccessAsync(string url, HttpStatusCode status) => (await GetWithRetry(url, expectedStatus: status)).Content;
48+
public virtual async Task<string> GetNonSuccessAsync(string url, HttpStatusCode status)
49+
{
50+
return (await _retryPolicy.HttpRetry(
51+
async () => await SendAsync(HttpMethod.Get, url, expectedStatus: status),
52+
ex => ex.StatusCode.HasValue && (int)ex.StatusCode.Value >= 500
53+
)).Content;
54+
}
4955

5056
public virtual async Task<string> GetAsync(string url, Dictionary<string, string> customHeaders = null) => (await GetWithRetry(url, customHeaders)).Content;
5157

src/OctoshiftCLI.Tests/Octoshift/Services/GithubClientTests.cs

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,27 +1063,150 @@ public async Task GetNonSuccessAsync_Logs_The_GitHub_Request_Id_Header_Value()
10631063
}
10641064

10651065
[Fact]
1066-
public async Task GetNonSuccessAsync_Retries_On_Non_Success()
1066+
public async Task GetNonSuccessAsync_Does_Not_Retry_On_Expected_Status()
10671067
{
10681068
// Arrange
10691069
var handlerMock = new Mock<HttpMessageHandler>();
10701070
handlerMock
10711071
.Protected()
1072-
.SetupSequence<Task<HttpResponseMessage>>(
1072+
.Setup<Task<HttpResponseMessage>>(
10731073
"SendAsync",
10741074
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
10751075
ItExpr.IsAny<CancellationToken>())
1076-
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.InternalServerError))
1077-
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.Found, content: EXPECTED_RESPONSE_CONTENT));
1076+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.NotFound, content: "Not Found"));
10781077

10791078
using var httpClient = new HttpClient(handlerMock.Object);
10801079
var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN);
10811080

10821081
// Act
1083-
var result = await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.Found);
1082+
var result = await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.NotFound);
1083+
1084+
// Assert
1085+
result.Should().Be("Not Found");
1086+
handlerMock.Protected().Verify(
1087+
"SendAsync",
1088+
Times.Once(),
1089+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1090+
ItExpr.IsAny<CancellationToken>());
1091+
}
1092+
1093+
[Fact]
1094+
public async Task GetNonSuccessAsync_Does_Not_Retry_On_Unexpected_Success_Status()
1095+
{
1096+
// Arrange
1097+
var handlerMock = new Mock<HttpMessageHandler>();
1098+
handlerMock
1099+
.Protected()
1100+
.Setup<Task<HttpResponseMessage>>(
1101+
"SendAsync",
1102+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1103+
ItExpr.IsAny<CancellationToken>())
1104+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.OK, content: "Success"));
1105+
1106+
using var httpClient = new HttpClient(handlerMock.Object);
1107+
var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN);
1108+
1109+
// Act & Assert - should throw immediately without retry
1110+
await FluentActions
1111+
.Invoking(async () => await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.NotFound))
1112+
.Should()
1113+
.ThrowExactlyAsync<HttpRequestException>()
1114+
.WithMessage("*OK*");
1115+
1116+
handlerMock.Protected().Verify(
1117+
"SendAsync",
1118+
Times.Once(),
1119+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1120+
ItExpr.IsAny<CancellationToken>());
1121+
}
1122+
1123+
[Fact]
1124+
public async Task GetNonSuccessAsync_Retries_On_Server_Error()
1125+
{
1126+
// Arrange
1127+
var handlerMock = new Mock<HttpMessageHandler>();
1128+
handlerMock
1129+
.Protected()
1130+
.SetupSequence<Task<HttpResponseMessage>>(
1131+
"SendAsync",
1132+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1133+
ItExpr.IsAny<CancellationToken>())
1134+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.InternalServerError, content: "Server Error"))
1135+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.NotFound, content: "Not Found"));
1136+
1137+
using var httpClient = new HttpClient(handlerMock.Object);
1138+
var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN);
1139+
1140+
// Act - should retry on 5xx and eventually succeed
1141+
var result = await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.NotFound);
10841142

10851143
// Assert
1086-
result.Should().Be(EXPECTED_RESPONSE_CONTENT);
1144+
result.Should().Be("Not Found");
1145+
handlerMock.Protected().Verify(
1146+
"SendAsync",
1147+
Times.Exactly(2),
1148+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1149+
ItExpr.IsAny<CancellationToken>());
1150+
}
1151+
1152+
[Fact]
1153+
public async Task GetNonSuccessAsync_Does_Not_Retry_On_Client_Error()
1154+
{
1155+
// Arrange
1156+
var handlerMock = new Mock<HttpMessageHandler>();
1157+
handlerMock
1158+
.Protected()
1159+
.Setup<Task<HttpResponseMessage>>(
1160+
"SendAsync",
1161+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1162+
ItExpr.IsAny<CancellationToken>())
1163+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.BadRequest, content: "Bad Request"));
1164+
1165+
using var httpClient = new HttpClient(handlerMock.Object);
1166+
var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN);
1167+
1168+
// Act & Assert - should throw immediately without retry on 4xx
1169+
await FluentActions
1170+
.Invoking(async () => await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.NotFound))
1171+
.Should()
1172+
.ThrowExactlyAsync<HttpRequestException>()
1173+
.WithMessage("*BadRequest*");
1174+
1175+
handlerMock.Protected().Verify(
1176+
"SendAsync",
1177+
Times.Once(),
1178+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1179+
ItExpr.IsAny<CancellationToken>());
1180+
}
1181+
1182+
[Fact]
1183+
public async Task GetNonSuccessAsync_Does_Not_Retry_On_Redirect_Status()
1184+
{
1185+
// Arrange
1186+
var handlerMock = new Mock<HttpMessageHandler>();
1187+
handlerMock
1188+
.Protected()
1189+
.Setup<Task<HttpResponseMessage>>(
1190+
"SendAsync",
1191+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1192+
ItExpr.IsAny<CancellationToken>())
1193+
.ReturnsAsync(CreateHttpResponseFactory(statusCode: HttpStatusCode.MovedPermanently, content: "Moved"));
1194+
1195+
using var httpClient = new HttpClient(handlerMock.Object);
1196+
var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN);
1197+
1198+
// Act & Assert - should throw immediately without retry on redirect
1199+
await FluentActions
1200+
.Invoking(async () => await githubClient.GetNonSuccessAsync(URL, HttpStatusCode.NotFound))
1201+
.Should()
1202+
.ThrowExactlyAsync<HttpRequestException>()
1203+
.WithMessage("*MovedPermanently*");
1204+
1205+
handlerMock.Protected().Verify(
1206+
"SendAsync",
1207+
Times.Once(),
1208+
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
1209+
ItExpr.IsAny<CancellationToken>());
10871210
}
10881211

10891212
[Fact]

0 commit comments

Comments
 (0)