Skip to content

Commit 9298136

Browse files
committed
Improvements in processing redirects with cookie containers.
1 parent 7e2cd26 commit 9298136

File tree

1 file changed

+74
-6
lines changed

1 file changed

+74
-6
lines changed

src/RestSharp/RestClient.Async.cs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
8888

8989
var httpMethod = AsHttpMethod(request.Method);
9090
var url = this.BuildUri(request);
91+
var originalUrl = url;
9192

9293
using var timeoutCts = new CancellationTokenSource(request.Timeout > 0 ? request.Timeout : int.MaxValue);
9394
using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
@@ -105,11 +106,19 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
105106
.AddCookieHeaders(url, cookieContainer)
106107
.AddCookieHeaders(url, Options.CookieContainer);
107108

108-
HttpResponseMessage? responseMessage;
109+
bool foundCookies = false;
110+
HttpResponseMessage? responseMessage = null;
109111

110-
while (true) {
112+
do {
111113
using var requestContent = new RequestContent(this, request);
112-
using var message = PrepareRequestMessage(httpMethod, url, requestContent, headers);
114+
using var content = requestContent.BuildContent();
115+
116+
// If we found coookies during a redirect,
117+
// we need to update the Cookie headers:
118+
if (foundCookies) {
119+
headers.AddCookieHeaders(cookieContainer, url);
120+
}
121+
using var message = PrepareRequestMessage(httpMethod, url, content, headers);
113122

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

@@ -132,19 +141,61 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
132141
location = new Uri(url, location);
133142
}
134143

144+
// Mirror HttpClient redirection behavior as of 07/25/2023:
145+
// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
146+
// fragment should inherit the fragment from the original URI.
147+
string requestFragment = originalUrl.Fragment;
148+
if (!string.IsNullOrEmpty(requestFragment)) {
149+
string redirectFragment = location.Fragment;
150+
if (string.IsNullOrEmpty(redirectFragment)) {
151+
location = new UriBuilder(location) { Fragment = requestFragment }.Uri;
152+
}
153+
}
154+
155+
// Disallow automatic redirection from secure to non-secure schemes
156+
// From HttpClient's RedirectHandler:
157+
//if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme) && !HttpUtilities.IsSupportedSecureScheme(location.Scheme)) {
158+
// if (NetEventSource.Log.IsEnabled()) {
159+
// TraceError($"Insecure https to http redirect from '{requestUri}' to '{location}' blocked.", response.RequestMessage!.GetHashCode());
160+
// }
161+
// break;
162+
//}
163+
135164
if (responseMessage.StatusCode == HttpStatusCode.RedirectMethod) {
136165
httpMethod = HttpMethod.Get;
137166
}
167+
// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
168+
// Many web browsers implemented this code in a manner that violated this standard, changing
169+
// the request type of the new request to GET, regardless of the type employed in the original request
170+
// (e.g. POST). For this reason, HTTP/1.1 (RFC 2616) added the new status codes 303 and 307 to disambiguate
171+
// between the two behaviours, with 303 mandating the change of request type to GET, and 307 preserving the
172+
// request type as originally sent. Despite the greater clarity provided by this disambiguation, the 302 code
173+
// is still employed in web frameworks to preserve compatibility with browsers that do not implement the HTTP/1.1
174+
// specification.
175+
176+
// NOTE: Given the above, it is not surprising that HttpClient when AllowRedirect = true
177+
// solves this problem by a helper method:
178+
if (RedirectRequestRequiresForceGet(responseMessage.StatusCode, httpMethod)) {
179+
httpMethod = HttpMethod.Get;
180+
// HttpClient sets request.Content to null here:
181+
// TODO: However... should we be allowed to modify Request like that here?
182+
message.Content = null;
183+
// HttpClient Redirect handler also does this:
184+
//if (message.Headers.TansferEncodingChunked == true) {
185+
// request.Headers.TransferEncodingChunked = false;
186+
//}
187+
}
138188

139189
url = location;
140190

141191
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
192+
foundCookies = true;
142193
// ReSharper disable once PossibleMultipleEnumeration
143194
cookieContainer.AddCookies(url, cookiesHeader);
144195
// ReSharper disable once PossibleMultipleEnumeration
145196
Options.CookieContainer?.AddCookies(url, cookiesHeader);
146197
}
147-
}
198+
} while (true);
148199

149200
// Parse all the cookies from the response and update the cookie jar with cookies
150201
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
@@ -163,8 +214,25 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
163214
}
164215
}
165216

166-
HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, RequestContent requestContent, RequestHeaders headers) {
167-
var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() };
217+
/// <summary>
218+
/// Based on .net core RedirectHandler class:
219+
/// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
220+
/// </summary>
221+
/// <param name="statusCode"></param>
222+
/// <param name="httpMethod"></param>
223+
/// <returns></returns>
224+
/// <exception cref="NotImplementedException"></exception>
225+
private bool RedirectRequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod httpMethod) {
226+
return statusCode switch {
227+
HttpStatusCode.Moved or HttpStatusCode.Found or HttpStatusCode.MultipleChoices
228+
=> httpMethod == HttpMethod.Post,
229+
HttpStatusCode.SeeOther => httpMethod != HttpMethod.Get && httpMethod != HttpMethod.Head,
230+
_ => false,
231+
};
232+
}
233+
234+
HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, HttpContent content, RequestHeaders headers) {
235+
var message = new HttpRequestMessage(httpMethod, url) { Content = content };
168236
message.Headers.Host = Options.BaseHost;
169237
message.Headers.CacheControl = Options.CachePolicy;
170238
message.AddHeaders(headers);

0 commit comments

Comments
 (0)