@@ -91,6 +91,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
91
91
92
92
var httpMethod = AsHttpMethod ( request . Method ) ;
93
93
var url = this . BuildUri ( request ) ;
94
+ var originalUrl = url ;
94
95
95
96
using var timeoutCts = new CancellationTokenSource ( request . Timeout > 0 ? request . Timeout : int . MaxValue ) ;
96
97
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( timeoutCts . Token , cancellationToken ) ;
@@ -108,14 +109,28 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
108
109
. AddCookieHeaders ( cookieContainer , url ) ;
109
110
110
111
if ( Options . CookieContainer != null ) {
111
- headers . AddCookieHeaders ( Options . CookieContainer , url ) ;
112
+ _ = headers . AddCookieHeaders ( Options . CookieContainer , url ) ;
112
113
}
113
114
114
- HttpResponseMessage ? responseMessage ;
115
+ bool foundCookies = false ;
116
+ HttpResponseMessage ? responseMessage = null ;
115
117
116
- while ( true ) {
118
+ do {
117
119
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 ) ;
119
134
120
135
if ( request . OnBeforeRequest != null ) await request . OnBeforeRequest ( message ) . ConfigureAwait ( false ) ;
121
136
@@ -138,23 +153,65 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
138
153
location = new Uri ( url , location ) ;
139
154
}
140
155
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
+
141
176
if ( responseMessage . StatusCode == HttpStatusCode . RedirectMethod ) {
142
177
httpMethod = HttpMethod . Get ;
143
178
}
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
+ }
144
200
145
201
url = location ;
146
202
147
203
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var ch ) ) {
148
204
foreach ( var header in ch ) {
149
205
try {
150
206
cookieContainer . SetCookies ( url , header ) ;
207
+ foundCookies = true ;
151
208
}
152
209
catch ( CookieException ) {
153
210
// Do not fail request if we cannot parse a cookie
154
211
}
155
212
}
156
213
}
157
- }
214
+ } while ( true ) ;
158
215
159
216
// Parse all the cookies from the response and update the cookie jar with cookies
160
217
if ( responseMessage . Headers . TryGetValues ( KnownHeaders . SetCookie , out var cookiesHeader ) ) {
@@ -175,8 +232,25 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
175
232
}
176
233
}
177
234
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 } ;
180
254
message . Headers . Host = Options . BaseHost ;
181
255
message . Headers . CacheControl = Options . CachePolicy ;
182
256
message . AddHeaders ( headers ) ;
0 commit comments