Skip to content

Commit 26c045b

Browse files
authored
Implement WebRequest CachePolicy (#60913)
1 parent 731364d commit 26c045b

File tree

4 files changed

+360
-1
lines changed

4 files changed

+360
-1
lines changed

src/libraries/System.Net.Requests/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,7 @@
270270
<data name="SystemNetRequests_PlatformNotSupported" xml:space="preserve">
271271
<value>System.Net.Requests is not supported on this platform.</value>
272272
</data>
273+
<data name="CacheEntryNotFound" xml:space="preserve">
274+
<value>The request was aborted: The request cache-only policy does not allow a network request and the response is not found in cache.</value>
275+
</data>
273276
</root>

src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Net.Cache;
99
using System.Net.Http;
10+
using System.Net.Http.Headers;
1011
using System.Net.Security;
1112
using System.Net.Sockets;
1213
using System.Runtime.Serialization;
@@ -689,7 +690,21 @@ public static int DefaultMaximumErrorResponseLength
689690
get; set;
690691
}
691692

692-
public static new RequestCachePolicy? DefaultCachePolicy { get; set; } = new RequestCachePolicy(RequestCacheLevel.BypassCache);
693+
private static RequestCachePolicy? _defaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
694+
private static bool _isDefaultCachePolicySet;
695+
696+
public static new RequestCachePolicy? DefaultCachePolicy
697+
{
698+
get
699+
{
700+
return _defaultCachePolicy;
701+
}
702+
set
703+
{
704+
_isDefaultCachePolicySet = true;
705+
_defaultCachePolicy = value;
706+
}
707+
}
693708

694709
public DateTime IfModifiedSince
695710
{
@@ -1137,6 +1152,8 @@ private async Task<WebResponse> SendRequest(bool async)
11371152
request.Headers.Host = Host;
11381153
}
11391154

1155+
AddCacheControlHeaders(request);
1156+
11401157
// Copy the HttpWebRequest request headers from the WebHeaderCollection into HttpRequestMessage.Headers and
11411158
// HttpRequestMessage.Content.Headers.
11421159
foreach (string headerName in _webHeaderCollection)
@@ -1202,6 +1219,118 @@ private async Task<WebResponse> SendRequest(bool async)
12021219
}
12031220
}
12041221

1222+
private void AddCacheControlHeaders(HttpRequestMessage request)
1223+
{
1224+
RequestCachePolicy? policy = GetApplicableCachePolicy();
1225+
1226+
if (policy != null && policy.Level != RequestCacheLevel.BypassCache)
1227+
{
1228+
CacheControlHeaderValue? cacheControl = null;
1229+
HttpHeaderValueCollection<NameValueHeaderValue> pragmaHeaders = request.Headers.Pragma;
1230+
1231+
if (policy is HttpRequestCachePolicy httpRequestCachePolicy)
1232+
{
1233+
switch (httpRequestCachePolicy.Level)
1234+
{
1235+
case HttpRequestCacheLevel.NoCacheNoStore:
1236+
cacheControl = new CacheControlHeaderValue
1237+
{
1238+
NoCache = true,
1239+
NoStore = true
1240+
};
1241+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
1242+
break;
1243+
case HttpRequestCacheLevel.Reload:
1244+
cacheControl = new CacheControlHeaderValue
1245+
{
1246+
NoCache = true
1247+
};
1248+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
1249+
break;
1250+
case HttpRequestCacheLevel.CacheOnly:
1251+
throw new WebException(SR.CacheEntryNotFound, WebExceptionStatus.CacheEntryNotFound);
1252+
case HttpRequestCacheLevel.CacheOrNextCacheOnly:
1253+
cacheControl = new CacheControlHeaderValue
1254+
{
1255+
OnlyIfCached = true
1256+
};
1257+
break;
1258+
case HttpRequestCacheLevel.Default:
1259+
cacheControl = new CacheControlHeaderValue();
1260+
1261+
if (httpRequestCachePolicy.MinFresh > TimeSpan.Zero)
1262+
{
1263+
cacheControl.MinFresh = httpRequestCachePolicy.MinFresh;
1264+
}
1265+
1266+
if (httpRequestCachePolicy.MaxAge != TimeSpan.MaxValue)
1267+
{
1268+
cacheControl.MaxAge = httpRequestCachePolicy.MaxAge;
1269+
}
1270+
1271+
if (httpRequestCachePolicy.MaxStale > TimeSpan.Zero)
1272+
{
1273+
cacheControl.MaxStale = true;
1274+
cacheControl.MaxStaleLimit = httpRequestCachePolicy.MaxStale;
1275+
}
1276+
1277+
break;
1278+
case HttpRequestCacheLevel.Refresh:
1279+
cacheControl = new CacheControlHeaderValue
1280+
{
1281+
MaxAge = TimeSpan.Zero
1282+
};
1283+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
1284+
break;
1285+
}
1286+
}
1287+
else
1288+
{
1289+
switch (policy.Level)
1290+
{
1291+
case RequestCacheLevel.NoCacheNoStore:
1292+
cacheControl = new CacheControlHeaderValue
1293+
{
1294+
NoCache = true,
1295+
NoStore = true
1296+
};
1297+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
1298+
break;
1299+
case RequestCacheLevel.Reload:
1300+
cacheControl = new CacheControlHeaderValue
1301+
{
1302+
NoCache = true
1303+
};
1304+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
1305+
break;
1306+
case RequestCacheLevel.CacheOnly:
1307+
throw new WebException(SR.CacheEntryNotFound, WebExceptionStatus.CacheEntryNotFound);
1308+
}
1309+
}
1310+
1311+
if (cacheControl != null)
1312+
{
1313+
request.Headers.CacheControl = cacheControl;
1314+
}
1315+
}
1316+
}
1317+
1318+
private RequestCachePolicy? GetApplicableCachePolicy()
1319+
{
1320+
if (CachePolicy != null)
1321+
{
1322+
return CachePolicy;
1323+
}
1324+
else if (_isDefaultCachePolicySet && DefaultCachePolicy != null)
1325+
{
1326+
return DefaultCachePolicy;
1327+
}
1328+
else
1329+
{
1330+
return WebRequest.DefaultCachePolicy;
1331+
}
1332+
}
1333+
12051334
public override IAsyncResult BeginGetResponse(AsyncCallback? callback, object? state)
12061335
{
12071336
CheckAbort();

src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,6 +1924,170 @@ public void Abort_CreateRequestThenAbort_Success(Uri remoteServer)
19241924
request.Abort();
19251925
}
19261926

1927+
[Theory]
1928+
[InlineData(HttpRequestCacheLevel.NoCacheNoStore, null, null, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache"})]
1929+
[InlineData(HttpRequestCacheLevel.Reload, null, null, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
1930+
[InlineData(HttpRequestCacheLevel.CacheOrNextCacheOnly, null, null, new string[] { "Cache-Control: only-if-cached" })]
1931+
[InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MinFresh, 10, new string[] { "Cache-Control: min-fresh=10" })]
1932+
[InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MaxAge, 10, new string[] { "Cache-Control: max-age=10" })]
1933+
[InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MaxStale, 10, new string[] { "Cache-Control: max-stale=10" })]
1934+
[InlineData(HttpRequestCacheLevel.Refresh, null, null, new string[] { "Pragma: no-cache", "Cache-Control: max-age=0" })]
1935+
public async Task SendHttpGetRequest_WithHttpCachePolicy_AddCacheHeaders(
1936+
HttpRequestCacheLevel requestCacheLevel, HttpCacheAgeControl? ageControl, int? age, string[] expectedHeaders)
1937+
{
1938+
await LoopbackServer.CreateServerAsync(async (server, uri) =>
1939+
{
1940+
HttpWebRequest request = WebRequest.CreateHttp(uri);
1941+
request.CachePolicy = ageControl != null ?
1942+
new HttpRequestCachePolicy(ageControl.Value, TimeSpan.FromSeconds((double)age))
1943+
: new HttpRequestCachePolicy(requestCacheLevel);
1944+
Task<WebResponse> getResponse = GetResponseAsync(request);
1945+
1946+
await server.AcceptConnectionAsync(async connection =>
1947+
{
1948+
List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
1949+
1950+
foreach (string header in expectedHeaders)
1951+
{
1952+
Assert.Contains(header, headers);
1953+
}
1954+
});
1955+
1956+
using (var response = (HttpWebResponse)await getResponse)
1957+
{
1958+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
1959+
}
1960+
});
1961+
}
1962+
1963+
[Theory]
1964+
[InlineData(RequestCacheLevel.NoCacheNoStore, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache" })]
1965+
[InlineData(RequestCacheLevel.Reload, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
1966+
public async Task SendHttpGetRequest_WithCachePolicy_AddCacheHeaders(
1967+
RequestCacheLevel requestCacheLevel, string[] expectedHeaders)
1968+
{
1969+
await LoopbackServer.CreateServerAsync(async (server, uri) =>
1970+
{
1971+
HttpWebRequest request = WebRequest.CreateHttp(uri);
1972+
request.CachePolicy = new RequestCachePolicy(requestCacheLevel);
1973+
Task<WebResponse> getResponse = GetResponseAsync(request);
1974+
1975+
await server.AcceptConnectionAsync(async connection =>
1976+
{
1977+
List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
1978+
1979+
foreach (string header in expectedHeaders)
1980+
{
1981+
Assert.Contains(header, headers);
1982+
}
1983+
});
1984+
1985+
using (var response = (HttpWebResponse)await getResponse)
1986+
{
1987+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
1988+
}
1989+
});
1990+
}
1991+
1992+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
1993+
[InlineData(RequestCacheLevel.NoCacheNoStore, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache" })]
1994+
[InlineData(RequestCacheLevel.Reload, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
1995+
public void SendHttpGetRequest_WithGlobalCachePolicy_AddCacheHeaders(
1996+
RequestCacheLevel requestCacheLevel, string[] expectedHeaders)
1997+
{
1998+
RemoteExecutor.Invoke(async (async, reqCacheLevel, eh0, eh1) =>
1999+
{
2000+
await LoopbackServer.CreateServerAsync(async (server, uri) =>
2001+
{
2002+
HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(Enum.Parse<RequestCacheLevel>(reqCacheLevel));
2003+
HttpWebRequest request = WebRequest.CreateHttp(uri);
2004+
Task<WebResponse> getResponse = bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse());
2005+
2006+
await server.AcceptConnectionAsync(async connection =>
2007+
{
2008+
List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
2009+
Assert.Contains(eh0, headers);
2010+
Assert.Contains(eh1, headers);
2011+
});
2012+
2013+
using (var response = (HttpWebResponse)await getResponse)
2014+
{
2015+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
2016+
}
2017+
});
2018+
}, (this is HttpWebRequestTest_Async).ToString(), requestCacheLevel.ToString(), expectedHeaders[0], expectedHeaders[1]).Dispose();
2019+
}
2020+
2021+
[Theory]
2022+
[InlineData(true)]
2023+
[InlineData(false)]
2024+
public async Task SendHttpGetRequest_WithCachePolicyCacheOnly_ThrowException(
2025+
bool isHttpCachePolicy)
2026+
{
2027+
HttpWebRequest request = WebRequest.CreateHttp("http://anything");
2028+
request.CachePolicy = isHttpCachePolicy ? new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheOnly)
2029+
: new RequestCachePolicy(RequestCacheLevel.CacheOnly);
2030+
WebException exception = await Assert.ThrowsAsync<WebException>(() => GetResponseAsync(request));
2031+
Assert.Equal(SR.CacheEntryNotFound, exception.Message);
2032+
}
2033+
2034+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
2035+
public void SendHttpGetRequest_WithGlobalCachePolicyBypassCache_DoNotAddCacheHeaders()
2036+
{
2037+
RemoteExecutor.Invoke(async () =>
2038+
{
2039+
await LoopbackServer.CreateServerAsync(async (server, uri) =>
2040+
{
2041+
HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
2042+
HttpWebRequest request = WebRequest.CreateHttp(uri);
2043+
Task<WebResponse> getResponse = request.GetResponseAsync();
2044+
2045+
await server.AcceptConnectionAsync(async connection =>
2046+
{
2047+
List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
2048+
2049+
foreach (string header in headers)
2050+
{
2051+
Assert.DoesNotContain("Pragma", header);
2052+
Assert.DoesNotContain("Cache-Control", header);
2053+
}
2054+
});
2055+
2056+
using (var response = (HttpWebResponse)await getResponse)
2057+
{
2058+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
2059+
}
2060+
});
2061+
}).Dispose();
2062+
}
2063+
2064+
[Fact]
2065+
public async Task SendHttpGetRequest_WithCachePolicyBypassCache_DoNotAddHeaders()
2066+
{
2067+
await LoopbackServer.CreateServerAsync(async (server, uri) =>
2068+
{
2069+
HttpWebRequest request = WebRequest.CreateHttp(uri);
2070+
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
2071+
Task<WebResponse> getResponse = request.GetResponseAsync();
2072+
2073+
await server.AcceptConnectionAsync(async connection =>
2074+
{
2075+
List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
2076+
2077+
foreach (string header in headers)
2078+
{
2079+
Assert.DoesNotContain("Pragma", header);
2080+
Assert.DoesNotContain("Cache-Control", header);
2081+
}
2082+
});
2083+
2084+
using (var response = (HttpWebResponse)await getResponse)
2085+
{
2086+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
2087+
}
2088+
});
2089+
}
2090+
19272091
private void RequestStreamCallback(IAsyncResult asynchronousResult)
19282092
{
19292093
RequestState state = (RequestState)asynchronousResult.AsyncState;

0 commit comments

Comments
 (0)