@@ -88,6 +88,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
88
88
89
89
var httpMethod = AsHttpMethod ( request . Method ) ;
90
90
var url = this . BuildUri ( request ) ;
91
+ var originalUrl = url ;
91
92
92
93
using var timeoutCts = new CancellationTokenSource ( request . Timeout > 0 ? request . Timeout : int . MaxValue ) ;
93
94
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( timeoutCts . Token , cancellationToken ) ;
@@ -105,11 +106,19 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
105
106
. AddCookieHeaders ( url , cookieContainer )
106
107
. AddCookieHeaders ( url , Options . CookieContainer ) ;
107
108
108
- HttpResponseMessage ? responseMessage ;
109
+ bool foundCookies = false ;
110
+ HttpResponseMessage ? responseMessage = null ;
109
111
110
- while ( true ) {
112
+ do {
111
113
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 ) ;
113
122
114
123
if ( request . OnBeforeRequest != null ) await request . OnBeforeRequest ( message ) . ConfigureAwait ( false ) ;
115
124
@@ -132,19 +141,61 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
132
141
location = new Uri ( url , location ) ;
133
142
}
134
143
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
+
135
164
if ( responseMessage . StatusCode == HttpStatusCode . RedirectMethod ) {
136
165
httpMethod = HttpMethod . Get ;
137
166
}
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
+ }
138
188
139
189
url = location ;
140
190
141
191
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
192
+ foundCookies = true ;
142
193
// ReSharper disable once PossibleMultipleEnumeration
143
194
cookieContainer . AddCookies ( url , cookiesHeader ) ;
144
195
// ReSharper disable once PossibleMultipleEnumeration
145
196
Options . CookieContainer ? . AddCookies ( url , cookiesHeader ) ;
146
197
}
147
- }
198
+ } while ( true ) ;
148
199
149
200
// Parse all the cookies from the response and update the cookie jar with cookies
150
201
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
@@ -163,8 +214,25 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
163
214
}
164
215
}
165
216
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 } ;
168
236
message . Headers . Host = Options . BaseHost ;
169
237
message . Headers . CacheControl = Options . CachePolicy ;
170
238
message . AddHeaders ( headers ) ;
0 commit comments