Skip to content

Commit d3727a0

Browse files
committed
Improvements in processing redirects with cookie containers.
1 parent 117feaa commit d3727a0

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

src/RestSharp/Request/RequestHeaders.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,10 @@ public RequestHeaders AddCookieHeaders(CookieContainer cookieContainer, Uri uri)
4848

4949
return this;
5050
}
51+
52+
public RequestHeaders RemoveCookieHeaders() {
53+
Parameters.RemoveParameter(KnownHeaders.Cookie);
54+
55+
return this;
56+
}
5157
}

src/RestSharp/RestClient.Async.cs

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

9292
var httpMethod = AsHttpMethod(request.Method);
9393
var url = this.BuildUri(request);
94+
var originalUrl = url;
9495

9596
using var timeoutCts = new CancellationTokenSource(request.Timeout > 0 ? request.Timeout : int.MaxValue);
9697
using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
@@ -108,14 +109,28 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
108109
.AddCookieHeaders(cookieContainer, url);
109110

110111
if (Options.CookieContainer != null) {
111-
headers.AddCookieHeaders(Options.CookieContainer, url);
112+
_ = headers.AddCookieHeaders(Options.CookieContainer, url);
112113
}
113114

114-
HttpResponseMessage? responseMessage;
115+
bool foundCookies = false;
116+
HttpResponseMessage? responseMessage = null;
115117

116-
while (true) {
118+
do {
117119
using var requestContent = new RequestContent(this, request);
118-
using var message = PrepareRequestMessage(httpMethod, url, requestContent, headers);
120+
using var content = requestContent.BuildContent();
121+
122+
// If we found coookies during a redirect,
123+
// we need to update the Cookie headers:
124+
if (foundCookies) {
125+
headers.RemoveCookieHeaders();
126+
headers.AddCookieHeaders(cookieContainer, url);
127+
// TODO: SHOULD this repreatedly re-add the option cookies? or
128+
// stick with the cookieContainer?
129+
if (Options.CookieContainer != null) {
130+
_ = headers.AddCookieHeaders(Options.CookieContainer, url);
131+
}
132+
}
133+
using var message = PrepareRequestMessage(httpMethod, url, content, headers);
119134

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

@@ -138,23 +153,65 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
138153
location = new Uri(url, location);
139154
}
140155

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

145201
url = location;
146202

147203
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var ch)) {
148204
foreach (var header in ch) {
149205
try {
150206
cookieContainer.SetCookies(url, header);
207+
foundCookies = true;
151208
}
152209
catch (CookieException) {
153210
// Do not fail request if we cannot parse a cookie
154211
}
155212
}
156213
}
157-
}
214+
} while (true);
158215

159216
// Parse all the cookies from the response and update the cookie jar with cookies
160217
if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) {
@@ -175,8 +232,25 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
175232
}
176233
}
177234

178-
HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, RequestContent requestContent, RequestHeaders headers) {
179-
var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() };
235+
/// <summary>
236+
/// Based on .net core RedirectHandler class:
237+
/// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
238+
/// </summary>
239+
/// <param name="statusCode"></param>
240+
/// <param name="httpMethod"></param>
241+
/// <returns></returns>
242+
/// <exception cref="NotImplementedException"></exception>
243+
private bool RedirectRequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod httpMethod) {
244+
return statusCode switch {
245+
HttpStatusCode.Moved or HttpStatusCode.Found or HttpStatusCode.MultipleChoices
246+
=> httpMethod == HttpMethod.Post,
247+
HttpStatusCode.SeeOther => httpMethod != HttpMethod.Get && httpMethod != HttpMethod.Head,
248+
_ => false,
249+
};
250+
}
251+
252+
HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, HttpContent content, RequestHeaders headers) {
253+
var message = new HttpRequestMessage(httpMethod, url) { Content = content };
180254
message.Headers.Host = Options.BaseHost;
181255
message.Headers.CacheControl = Options.CachePolicy;
182256
message.AddHeaders(headers);

0 commit comments

Comments
 (0)