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