@@ -89,6 +89,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
8989
9090 var httpMethod = AsHttpMethod ( request . Method ) ;
9191 var url = this . BuildUri ( request ) ;
92+ var originalUrl = url ;
9293
9394 using var timeoutCts = new CancellationTokenSource ( request . Timeout > 0 ? request . Timeout : int . MaxValue ) ;
9495 using var cts = CancellationTokenSource . CreateLinkedTokenSource ( timeoutCts . Token , cancellationToken ) ;
@@ -122,11 +123,19 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
122123 . AddCookieHeaders ( url , cookieContainer )
123124 . AddCookieHeaders ( url , Options . CookieContainer ) ;
124125
125- HttpResponseMessage ? responseMessage ;
126+ bool foundCookies = false ;
127+ HttpResponseMessage ? responseMessage = null ;
126128
127- while ( true ) {
129+ do {
128130 using var requestContent = new RequestContent ( this , request ) ;
129- using var message = PrepareRequestMessage ( httpMethod , url , requestContent , headers ) ;
131+ using var content = requestContent . BuildContent ( ) ;
132+
133+ // If we found coookies during a redirect,
134+ // we need to update the Cookie headers:
135+ if ( foundCookies ) {
136+ headers . AddCookieHeaders ( cookieContainer , url ) ;
137+ }
138+ using var message = PrepareRequestMessage ( httpMethod , url , content , headers ) ;
130139
131140 if ( request . OnBeforeRequest != null ) await request . OnBeforeRequest ( message ) . ConfigureAwait ( false ) ;
132141
@@ -149,19 +158,61 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
149158 location = new Uri ( url , location ) ;
150159 }
151160
161+ // Mirror HttpClient redirection behavior as of 07/25/2023:
162+ // Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
163+ // fragment should inherit the fragment from the original URI.
164+ string requestFragment = originalUrl . Fragment ;
165+ if ( ! string . IsNullOrEmpty ( requestFragment ) ) {
166+ string redirectFragment = location . Fragment ;
167+ if ( string . IsNullOrEmpty ( redirectFragment ) ) {
168+ location = new UriBuilder ( location ) { Fragment = requestFragment } . Uri ;
169+ }
170+ }
171+
172+ // Disallow automatic redirection from secure to non-secure schemes
173+ // From HttpClient's RedirectHandler:
174+ //if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme) && !HttpUtilities.IsSupportedSecureScheme(location.Scheme)) {
175+ // if (NetEventSource.Log.IsEnabled()) {
176+ // TraceError($"Insecure https to http redirect from '{requestUri}' to '{location}' blocked.", response.RequestMessage!.GetHashCode());
177+ // }
178+ // break;
179+ //}
180+
152181 if ( responseMessage . StatusCode == HttpStatusCode . RedirectMethod ) {
153182 httpMethod = HttpMethod . Get ;
154183 }
184+ // Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
185+ // Many web browsers implemented this code in a manner that violated this standard, changing
186+ // the request type of the new request to GET, regardless of the type employed in the original request
187+ // (e.g. POST). For this reason, HTTP/1.1 (RFC 2616) added the new status codes 303 and 307 to disambiguate
188+ // between the two behaviours, with 303 mandating the change of request type to GET, and 307 preserving the
189+ // request type as originally sent. Despite the greater clarity provided by this disambiguation, the 302 code
190+ // is still employed in web frameworks to preserve compatibility with browsers that do not implement the HTTP/1.1
191+ // specification.
192+
193+ // NOTE: Given the above, it is not surprising that HttpClient when AllowRedirect = true
194+ // solves this problem by a helper method:
195+ if ( RedirectRequestRequiresForceGet ( responseMessage . StatusCode , httpMethod ) ) {
196+ httpMethod = HttpMethod . Get ;
197+ // HttpClient sets request.Content to null here:
198+ // TODO: However... should we be allowed to modify Request like that here?
199+ message . Content = null ;
200+ // HttpClient Redirect handler also does this:
201+ //if (message.Headers.TansferEncodingChunked == true) {
202+ // request.Headers.TransferEncodingChunked = false;
203+ //}
204+ }
155205
156206 url = location ;
157207
158208 if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
209+ foundCookies = true ;
159210 // ReSharper disable once PossibleMultipleEnumeration
160211 cookieContainer . AddCookies ( url , cookiesHeader ) ;
161212 // ReSharper disable once PossibleMultipleEnumeration
162213 Options . CookieContainer ? . AddCookies ( url , cookiesHeader ) ;
163214 }
164- }
215+ } while ( true ) ;
165216
166217 // Parse all the cookies from the response and update the cookie jar with cookies
167218 if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
@@ -208,8 +259,25 @@ async Task OnAfterRequest(HttpResponseMessage responseMessage) {
208259 }
209260 }
210261
211- HttpRequestMessage PrepareRequestMessage ( HttpMethod httpMethod , Uri url , RequestContent requestContent , RequestHeaders headers ) {
212- var message = new HttpRequestMessage ( httpMethod , url ) { Content = requestContent . BuildContent ( ) } ;
262+ /// <summary>
263+ /// Based on .net core RedirectHandler class:
264+ /// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
265+ /// </summary>
266+ /// <param name="statusCode"></param>
267+ /// <param name="httpMethod"></param>
268+ /// <returns></returns>
269+ /// <exception cref="NotImplementedException"></exception>
270+ private bool RedirectRequestRequiresForceGet ( HttpStatusCode statusCode , HttpMethod httpMethod ) {
271+ return statusCode switch {
272+ HttpStatusCode . Moved or HttpStatusCode . Found or HttpStatusCode . MultipleChoices
273+ => httpMethod == HttpMethod . Post ,
274+ HttpStatusCode . SeeOther => httpMethod != HttpMethod . Get && httpMethod != HttpMethod . Head ,
275+ _ => false ,
276+ } ;
277+ }
278+
279+ HttpRequestMessage PrepareRequestMessage ( HttpMethod httpMethod , Uri url , HttpContent content , RequestHeaders headers ) {
280+ var message = new HttpRequestMessage ( httpMethod , url ) { Content = content } ;
213281 message . Headers . Host = Options . BaseHost ;
214282 message . Headers . CacheControl = Options . CachePolicy ;
215283 message . AddHeaders ( headers ) ;
0 commit comments