@@ -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