Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix redirect timeout on renamed repositories #1411

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dcf1cf0
Merge remote-tracking branch 'refs/remotes/octokit/master'
martinscholz83 Jun 28, 2016
bce9696
add test
martinscholz83 Jun 28, 2016
61dfff8
Merge remote-tracking branch 'refs/remotes/octokit/master'
martinscholz83 Jun 29, 2016
dd38e4c
Merge remote-tracking branch 'refs/remotes/octokit/master' into fix-t…
martinscholz83 Jun 29, 2016
dc65dab
Merge remote-tracking branch 'refs/remotes/octokit/master'
martinscholz83 Jun 30, 2016
f6191d2
Merge branch 'master' of https://github.com/maddin2016/octokit.net
martinscholz83 Jun 30, 2016
7f8cfba
Merge remote-tracking branch 'refs/remotes/octokit/master' into fix-t…
martinscholz83 Jul 4, 2016
84f0e7b
[WIP]
martinscholz83 Jul 4, 2016
b4d0ea9
put logic for redirects outside of delegating handler
martinscholz83 Jul 5, 2016
d2748c3
change send method
martinscholz83 Jul 5, 2016
f3efff1
format code
martinscholz83 Jul 5, 2016
b42240b
reorganized http client adapter
martinscholz83 Jul 5, 2016
c85508e
Merge remote-tracking branch 'refs/remotes/octokit/master' into fix-t…
martinscholz83 Jul 18, 2016
e6309cb
change HttpClientAdapter
martinscholz83 Jul 18, 2016
54225c9
rework http redirect tests - still an issue with accessing the respon…
ryangribble Jul 16, 2016
efe587b
Merge remote-tracking branch 'refs/remotes/octokit/master' into fix-t…
martinscholz83 Jul 23, 2016
2f4e03e
Merge branch 'fix-timeout-getting-multiple-repositories' of https://g…
martinscholz83 Jul 23, 2016
ff58f05
Merge remote-tracking branch 'refs/remotes/octokit/master' into fix-t…
martinscholz83 Jul 29, 2016
802091c
remove some unused lines in httpclientadapter
martinscholz83 Aug 10, 2016
815f0d1
Reworked redirect implementation to fully clone http request and re-u…
ryangribble Aug 10, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,31 @@ public async Task ReturnsOrganizationRepository()
Assert.Equal(AccountType.Organization, repository.Owner.Type);
}

[IntegrationTest]
public async Task ReturnsRenamedRepository()
{
var github = Helper.GetAuthenticatedClient();

var repository = await github.Repository.Get("michael-wolfenden", "Polly");

Assert.Equal("https://github.com/App-vNext/Polly.git", repository.CloneUrl);
Assert.False(repository.Private);
Assert.False(repository.Fork);
//Assert.Equal(AccountType.User, repository.Owner.Type);

repository = await github.Repository.Get("fsprojects", "FSharp.Atom");

Assert.Equal("https://github.com/ionide/ionide-atom-fsharp.git", repository.CloneUrl);
Assert.False(repository.Private);
Assert.False(repository.Fork);

repository = await github.Repository.Get("cabbage89", "Orchard.Weixin");

Assert.Equal("https://github.com/cabbage89/Orchard.WeChat.git", repository.CloneUrl);
Assert.False(repository.Private);
Assert.False(repository.Fork);
}

[IntegrationTest]
public async Task ReturnsOrganizationRepositoryWithRepositoryId()
{
Expand Down
2 changes: 1 addition & 1 deletion Octokit.Tests.Integration/RedirectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task ReturnsRedirectedRepository()
Assert.Equal(AccountType.User, repository.Owner.Type);
}

[IntegrationTest(Skip = "This test is super-unreliable right now - see https://github.com/octokit/octokit.net/issues/874 for discussion")]
[IntegrationTest]
public async Task CanCreateIssueOnRedirectedRepository()
{
var client = Helper.GetAuthenticatedClient();
Expand Down
70 changes: 36 additions & 34 deletions Octokit.Tests/Http/RedirectHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ public class RedirectHandlerTests
[Fact]
public async Task OkStatusShouldPassThrough()
{
var invoker = CreateInvoker(new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Get);

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.Equal(response.StatusCode, HttpStatusCode.OK);
Assert.Same(response.RequestMessage, httpRequestMessage);
Expand All @@ -32,11 +34,12 @@ public async Task ShouldRedirectSameMethod(HttpStatusCode statusCode)
var redirectResponse = new HttpResponseMessage(statusCode);
redirectResponse.Headers.Location = new Uri("http://example.org/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Post);

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.Equal(response.RequestMessage.Method, httpRequestMessage.Method);
Assert.NotSame(response.RequestMessage, httpRequestMessage);
Expand All @@ -48,11 +51,12 @@ public async Task Status303ShouldRedirectChangeMethod()
var redirectResponse = new HttpResponseMessage(HttpStatusCode.SeeOther);
redirectResponse.Headers.Location = new Uri("http://example.org/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Post);

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.Equal(HttpMethod.Get, response.RequestMessage.Method);
Assert.NotSame(response.RequestMessage, httpRequestMessage);
Expand All @@ -64,12 +68,13 @@ public async Task RedirectWithSameHostShouldKeepAuthHeader()
var redirectResponse = new HttpResponseMessage(HttpStatusCode.Redirect);
redirectResponse.Headers.Location = new Uri("http://example.org/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Get);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("fooAuth", "aparam");

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.NotSame(response.RequestMessage, httpRequestMessage);
Assert.Equal("fooAuth", response.RequestMessage.Headers.Authorization.Scheme);
Expand All @@ -86,12 +91,13 @@ public async Task RedirectWithDifferentHostShouldLoseAuthHeader(HttpStatusCode s
var redirectResponse = new HttpResponseMessage(statusCode);
redirectResponse.Headers.Location = new Uri("http://example.net/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Get);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("fooAuth", "aparam");

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.NotSame(response.RequestMessage, httpRequestMessage);
Assert.Null(response.RequestMessage.Headers.Authorization);
Expand All @@ -106,16 +112,16 @@ public async Task Status301ShouldRedirectPOSTWithBody(HttpStatusCode statusCode)
var redirectResponse = new HttpResponseMessage(statusCode);
redirectResponse.Headers.Location = new Uri("http://example.org/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Post);
httpRequestMessage.Content = new StringContent("Hello World");

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());

var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());
Assert.Equal(response.RequestMessage.Method, httpRequestMessage.Method);
Assert.NotSame(response.RequestMessage, httpRequestMessage);
Assert.Equal("Hello World", await response.RequestMessage.Content.ReadAsStringAsync());
}

// POST see other with content
Expand All @@ -125,12 +131,13 @@ public async Task Status303ShouldRedirectToGETWithoutBody()
var redirectResponse = new HttpResponseMessage(HttpStatusCode.SeeOther);
redirectResponse.Headers.Location = new Uri("http://example.org/bar");

var invoker = CreateInvoker(redirectResponse,
new HttpResponseMessage(HttpStatusCode.OK));
var handler = CreateMockHttpHandler(redirectResponse, new HttpResponseMessage(HttpStatusCode.OK));
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Post);
httpRequestMessage.Content = new StringContent("Hello World");

var response = await invoker.SendAsync(httpRequestMessage, new CancellationToken());
var response = await adapter.SendAsync(httpRequestMessage, new CancellationToken());

Assert.Equal(HttpMethod.Get, response.RequestMessage.Method);
Assert.NotSame(response.RequestMessage, httpRequestMessage);
Expand All @@ -145,14 +152,14 @@ public async Task Exceed3RedirectsShouldReturn()

var redirectResponse2 = new HttpResponseMessage(HttpStatusCode.Found);
redirectResponse2.Headers.Location = new Uri("http://example.org/foo");


var invoker = CreateInvoker(redirectResponse, redirectResponse2);
var handler = CreateMockHttpHandler(redirectResponse, redirectResponse2);
var adapter = new HttpClientAdapter(handler);

var httpRequestMessage = CreateRequest(HttpMethod.Get);

Assert.ThrowsAsync<InvalidOperationException>(
() => invoker.SendAsync(httpRequestMessage, new CancellationToken()));
() => adapter.SendAsync(httpRequestMessage, new CancellationToken()));
}

static HttpRequestMessage CreateRequest(HttpMethod method)
Expand All @@ -163,14 +170,9 @@ static HttpRequestMessage CreateRequest(HttpMethod method)
return httpRequestMessage;
}

static HttpMessageInvoker CreateInvoker(HttpResponseMessage httpResponseMessage1, HttpResponseMessage httpResponseMessage2 = null)
static Func<HttpMessageHandler> CreateMockHttpHandler(HttpResponseMessage httpResponseMessage1, HttpResponseMessage httpResponseMessage2 = null)
{
var redirectHandler = new RedirectHandler
{
InnerHandler = new MockRedirectHandler(httpResponseMessage1, httpResponseMessage2)
};
var invoker = new HttpMessageInvoker(redirectHandler);
return invoker;
return () => new MockRedirectHandler(httpResponseMessage1, httpResponseMessage2);
}
}

Expand Down
105 changes: 61 additions & 44 deletions Octokit/Http/HttpClientAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class HttpClientAdapter : IHttpClient
{
readonly HttpClient _http;

public const string RedirectCountKey = "RedirectCount";

[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
public HttpClientAdapter(Func<HttpMessageHandler> getHandler)
{
Expand All @@ -42,8 +44,8 @@ public async Task<IResponse> Send(IRequest request, CancellationToken cancellati

using (var requestMessage = BuildRequestMessage(request))
{
// Make the request
var responseMessage = await _http.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationTokenForRequest).ConfigureAwait(false);
var responseMessage = await SendAsync(requestMessage, cancellationTokenForRequest).ConfigureAwait(false);

return await BuildResponse(responseMessage).ConfigureAwait(false);
}
}
Expand Down Expand Up @@ -168,19 +170,20 @@ protected virtual void Dispose(bool disposing)
if (_http != null) _http.Dispose();
}
}
}

internal class RedirectHandler : DelegatingHandler
{
public const string RedirectCountKey = "RedirectCount";
public bool Enabled { get; set; }

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Clone the request/content incase we get a redirect
var clonedRequest = await CloneHttpRequestMessageAsync(request);

// Send initial response
var response = await _http.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);

// Can't redirect without somewhere to redirect too. Throw?
if (response.Headers.Location == null) return response;
// Can't redirect without somewhere to redirect to.
if (response.Headers.Location == null)
{
return response;
}

// Don't redirect if we exceed max number of redirects
var redirectCount = 0;
Expand All @@ -192,7 +195,6 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
throw new InvalidOperationException("The redirect count for this request has been exceeded. Aborting.");
}
request.Properties[RedirectCountKey] = ++redirectCount;

if (response.StatusCode == HttpStatusCode.MovedPermanently
|| response.StatusCode == HttpStatusCode.Redirect
Expand All @@ -201,55 +203,70 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
|| response.StatusCode == HttpStatusCode.TemporaryRedirect
|| (int)response.StatusCode == 308)
{
var newRequest = CopyRequest(response.RequestMessage);

if (response.StatusCode == HttpStatusCode.SeeOther)
{
newRequest.Content = null;
newRequest.Method = HttpMethod.Get;
clonedRequest.Content = null;
clonedRequest.Method = HttpMethod.Get;
}
else
{
if (request.Content != null && request.Content.Headers.ContentLength != 0)
{
var stream = await request.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (stream.CanSeek)
{
stream.Position = 0;
}
else
{
throw new Exception("Cannot redirect a request with an unbuffered body");
}
newRequest.Content = new StreamContent(stream);
}
}
newRequest.RequestUri = response.Headers.Location;
if (string.Compare(newRequest.RequestUri.Host, request.RequestUri.Host, StringComparison.OrdinalIgnoreCase) != 0)

// Increment the redirect count
clonedRequest.Properties[RedirectCountKey] = ++redirectCount;

// Set the new Uri based on location header
clonedRequest.RequestUri = response.Headers.Location;

// Clear authentication if redirected to a different host
if (string.Compare(clonedRequest.RequestUri.Host, request.RequestUri.Host, StringComparison.OrdinalIgnoreCase) != 0)
{
newRequest.Headers.Authorization = null;
clonedRequest.Headers.Authorization = null;
}
response = await SendAsync(newRequest, cancellationToken).ConfigureAwait(false);

// Send redirected request
response = await SendAsync(clonedRequest, cancellationToken).ConfigureAwait(false);
}

return response;
}

[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
private static HttpRequestMessage CopyRequest(HttpRequestMessage oldRequest)
public static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage oldRequest)
{
var newrequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);
var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);

// Copy the request's content (via a MemoryStream) into the cloned object
var ms = new MemoryStream();
if (oldRequest.Content != null)
{
await oldRequest.Content.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
newRequest.Content = new StreamContent(ms);

// Copy the content headers
if (oldRequest.Content.Headers != null)
{
foreach (var h in oldRequest.Content.Headers)
{
newRequest.Content.Headers.Add(h.Key, h.Value);
}
}
}

newRequest.Version = oldRequest.Version;

foreach (var header in oldRequest.Headers)
{
newrequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
}

foreach (var property in oldRequest.Properties)
{
newrequest.Properties.Add(property);
newRequest.Properties.Add(property);
}

return newrequest;
return newRequest;
}
}
}

internal class RedirectHandler : DelegatingHandler
{
}
}