@@ -89,6 +89,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
89
89
90
90
var httpMethod = AsHttpMethod ( request . Method ) ;
91
91
var url = this . BuildUri ( request ) ;
92
+ var originalUrl = url ;
92
93
93
94
using var timeoutCts = new CancellationTokenSource ( request . Timeout > 0 ? request . Timeout : int . MaxValue ) ;
94
95
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( timeoutCts . Token , cancellationToken ) ;
@@ -122,11 +123,19 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
122
123
. AddCookieHeaders ( url , cookieContainer )
123
124
. AddCookieHeaders ( url , Options . CookieContainer ) ;
124
125
125
- HttpResponseMessage ? responseMessage ;
126
+ bool foundCookies = false ;
127
+ HttpResponseMessage ? responseMessage = null ;
126
128
127
- while ( true ) {
129
+ do {
128
130
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 ) ;
130
139
131
140
if ( request . OnBeforeRequest != null ) await request . OnBeforeRequest ( message ) . ConfigureAwait ( false ) ;
132
141
@@ -149,19 +158,61 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
149
158
location = new Uri ( url , location ) ;
150
159
}
151
160
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
+
152
181
if ( responseMessage . StatusCode == HttpStatusCode . RedirectMethod ) {
153
182
httpMethod = HttpMethod . Get ;
154
183
}
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
+ }
155
205
156
206
url = location ;
157
207
158
208
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
209
+ foundCookies = true ;
159
210
// ReSharper disable once PossibleMultipleEnumeration
160
211
cookieContainer . AddCookies ( url , cookiesHeader ) ;
161
212
// ReSharper disable once PossibleMultipleEnumeration
162
213
Options . CookieContainer ? . AddCookies ( url , cookiesHeader ) ;
163
214
}
164
- }
215
+ } while ( true ) ;
165
216
166
217
// Parse all the cookies from the response and update the cookie jar with cookies
167
218
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
@@ -208,8 +259,25 @@ async Task OnAfterRequest(HttpResponseMessage responseMessage) {
208
259
}
209
260
}
210
261
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 } ;
213
281
message . Headers . Host = Options . BaseHost ;
214
282
message . Headers . CacheControl = Options . CachePolicy ;
215
283
message . AddHeaders ( headers ) ;
0 commit comments