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

Follow redirects #2058

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/RestSharp/Request/RestRequestExtensions.cs
Expand Up @@ -338,7 +338,7 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, Paramet
DataFormat.Json => request.AddJsonBody(obj, contentType),
DataFormat.Xml => request.AddXmlBody(obj, contentType),
DataFormat.Binary => request.AddParameter(new BodyParameter("", obj, ContentType.Binary)),
_ => request.AddParameter(new BodyParameter("", obj.ToString(), ContentType.Plain))
_ => request.AddParameter(new BodyParameter("", obj.ToString()!, ContentType.Plain))
};
}

Expand Down
73 changes: 63 additions & 10 deletions src/RestSharp/RestClient.Async.cs
Expand Up @@ -86,16 +86,11 @@ public partial class RestClient {
throw new ObjectDisposedException(nameof(RestClient));
}

using var requestContent = new RequestContent(this, request);

var authenticator = request.Authenticator ?? Options.Authenticator;
if (authenticator != null) await authenticator.Authenticate(this, request).ConfigureAwait(false);

var httpMethod = AsHttpMethod(request.Method);
var url = this.BuildUri(request);
var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() };
message.Headers.Host = Options.BaseHost;
message.Headers.CacheControl = Options.CachePolicy;

using var timeoutCts = new CancellationTokenSource(request.Timeout > 0 ? request.Timeout : int.MaxValue);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
Expand All @@ -116,11 +111,50 @@ public partial class RestClient {
headers.AddCookieHeaders(Options.CookieContainer, url);
}

message.AddHeaders(headers);
HttpResponseMessage? responseMessage;

while (true) {
using var requestContent = new RequestContent(this, request);
using var message = PrepareRequestMessage(httpMethod, url, requestContent, headers);

if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);

responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);

if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);

if (!IsRedirect(responseMessage)) {
// || !Options.FollowRedirects) {
break;
}

var location = responseMessage.Headers.Location;

if (location == null) {
break;
}

if (!location.IsAbsoluteUri) {
location = new Uri(url, location);
}

if (responseMessage.StatusCode == HttpStatusCode.RedirectMethod) {
httpMethod = HttpMethod.Get;
}

var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);
url = location;

if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var ch)) {
foreach (var header in ch) {
try {
cookieContainer.SetCookies(url, header);
}
catch (CookieException) {
// Do not fail request if we cannot parse a cookie
}
}
}
}

// Parse all the cookies from the response and update the cookie jar with cookies
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
Expand All @@ -134,15 +168,34 @@ public partial class RestClient {
}
}

if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);

return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token);
}
catch (Exception ex) {
return new HttpResponse(null, url, null, ex, timeoutCts.Token);
}
}

HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, RequestContent requestContent, RequestHeaders headers) {
var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() };
message.Headers.Host = Options.BaseHost;
message.Headers.CacheControl = Options.CachePolicy;
message.AddHeaders(headers);

return message;
}

static bool IsRedirect(HttpResponseMessage responseMessage)
=> responseMessage.StatusCode switch {
HttpStatusCode.MovedPermanently => true,
HttpStatusCode.SeeOther => true,
HttpStatusCode.TemporaryRedirect => true,
HttpStatusCode.Redirect => true,
#if NET
HttpStatusCode.PermanentRedirect => true,
#endif
_ => false
};

record HttpResponse(
HttpResponseMessage? ResponseMessage,
Uri Url,
Expand Down
44 changes: 44 additions & 0 deletions test/RestSharp.Tests.Integrated/RedirectTests.cs
@@ -0,0 +1,44 @@
using System.Net;
using RestSharp.Tests.Integrated.Server;

namespace RestSharp.Tests.Integrated;

[Collection(nameof(TestServerCollection))]
public class RedirectTests {
readonly RestClient _client;
readonly string _host;

public RedirectTests(TestServerFixture fixture, ITestOutputHelper output) {
var options = new RestClientOptions(fixture.Server.Url) {
FollowRedirects = false
};
_client = new RestClient(options);
_host = _client.Options.BaseUrl!.Host;
}

[Fact]
public async Task Can_Perform_GET_Async_With_Redirect() {
const string val = "Works!";

var request = new RestRequest("redirect");

var response = await _client.ExecuteAsync<Response>(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Data!.Message.Should().Be(val);
}

[Fact]
public async Task Can_Perform_GET_Async_With_Request_Cookies() {
var request = new RestRequest("get-cookies-redirect") {
CookieContainer = new CookieContainer(),
};
request.CookieContainer.Add(new Cookie("cookie", "value", null, _host));
request.CookieContainer.Add(new Cookie("cookie2", "value2", null, _host));
var response = await _client.ExecuteAsync(request);
response.Content.Should().Be("[\"cookie=value\",\"cookie2=value2\"]");
}

class Response {
public string? Message { get; set; }
}
}
10 changes: 10 additions & 0 deletions test/RestSharp.Tests.Integrated/Server/TestServer.cs
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Logging;
using RestSharp.Tests.Integrated.Server.Handlers;
using RestSharp.Tests.Shared.Extensions;

// ReSharper disable ConvertClosureToMethodGroup

namespace RestSharp.Tests.Integrated.Server;
Expand Down Expand Up @@ -36,11 +37,20 @@ public sealed class HttpServer {
_app.MapGet("headers", HeaderHandlers.HandleHeaders);
_app.MapGet("request-echo", async context => await context.Request.BodyReader.AsStream().CopyToAsync(context.Response.BodyWriter.AsStream()));
_app.MapDelete("delete", () => new TestResponse { Message = "Works!" });
_app.MapGet("redirect", () => Results.Redirect("/success", false, true));

// Cookies
_app.MapGet("get-cookies", CookieHandlers.HandleCookies);
_app.MapGet("set-cookies", CookieHandlers.HandleSetCookies);

_app.MapGet(
"get-cookies-redirect",
(HttpContext ctx) => {
ctx.Response.Cookies.Append("redirectCookie", "value1");
return Results.Redirect("/get-cookies", false, true);
}
);

// PUT
_app.MapPut(
ContentResource,
Expand Down