Implement WebRequest CachePolicy (#60913)
authorBadre BSAILA <54767641+pedrobsaila@users.noreply.github.com>
Tue, 9 Nov 2021 19:38:32 +0000 (20:38 +0100)
committerGitHub <noreply@github.com>
Tue, 9 Nov 2021 19:38:32 +0000 (11:38 -0800)
src/libraries/System.Net.Requests/src/Resources/Strings.resx
src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs
src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs
src/libraries/System.Net.Requests/tests/WebRequestTest.cs

index 5240ec2..a3b2bb9 100644 (file)
   <data name="SystemNetRequests_PlatformNotSupported" xml:space="preserve">
     <value>System.Net.Requests is not supported on this platform.</value>
   </data>
+  <data name="CacheEntryNotFound" xml:space="preserve">
+    <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>
+  </data>
 </root>
index 3adae08..c92ea9f 100644 (file)
@@ -7,6 +7,7 @@ using System.Globalization;
 using System.IO;
 using System.Net.Cache;
 using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Net.Security;
 using System.Net.Sockets;
 using System.Runtime.Serialization;
@@ -689,7 +690,21 @@ namespace System.Net
             get; set;
         }
 
-        public static new RequestCachePolicy? DefaultCachePolicy { get; set; } = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+        private static RequestCachePolicy? _defaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+        private static bool _isDefaultCachePolicySet;
+
+        public static new RequestCachePolicy? DefaultCachePolicy
+        {
+            get
+            {
+                return _defaultCachePolicy;
+            }
+            set
+            {
+                _isDefaultCachePolicySet = true;
+                _defaultCachePolicy = value;
+            }
+        }
 
         public DateTime IfModifiedSince
         {
@@ -1137,6 +1152,8 @@ namespace System.Net
                     request.Headers.Host = Host;
                 }
 
+                AddCacheControlHeaders(request);
+
                 // Copy the HttpWebRequest request headers from the WebHeaderCollection into HttpRequestMessage.Headers and
                 // HttpRequestMessage.Content.Headers.
                 foreach (string headerName in _webHeaderCollection)
@@ -1202,6 +1219,118 @@ namespace System.Net
             }
         }
 
+        private void AddCacheControlHeaders(HttpRequestMessage request)
+        {
+            RequestCachePolicy? policy = GetApplicableCachePolicy();
+
+            if (policy != null && policy.Level != RequestCacheLevel.BypassCache)
+            {
+                CacheControlHeaderValue? cacheControl = null;
+                HttpHeaderValueCollection<NameValueHeaderValue> pragmaHeaders = request.Headers.Pragma;
+
+                if (policy is HttpRequestCachePolicy httpRequestCachePolicy)
+                {
+                    switch (httpRequestCachePolicy.Level)
+                    {
+                        case HttpRequestCacheLevel.NoCacheNoStore:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                NoCache = true,
+                                NoStore = true
+                            };
+                            pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
+                            break;
+                        case HttpRequestCacheLevel.Reload:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                NoCache = true
+                            };
+                            pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
+                            break;
+                        case HttpRequestCacheLevel.CacheOnly:
+                            throw new WebException(SR.CacheEntryNotFound, WebExceptionStatus.CacheEntryNotFound);
+                        case HttpRequestCacheLevel.CacheOrNextCacheOnly:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                OnlyIfCached = true
+                            };
+                            break;
+                        case HttpRequestCacheLevel.Default:
+                            cacheControl = new CacheControlHeaderValue();
+
+                            if (httpRequestCachePolicy.MinFresh > TimeSpan.Zero)
+                            {
+                                cacheControl.MinFresh = httpRequestCachePolicy.MinFresh;
+                            }
+
+                            if (httpRequestCachePolicy.MaxAge != TimeSpan.MaxValue)
+                            {
+                                cacheControl.MaxAge = httpRequestCachePolicy.MaxAge;
+                            }
+
+                            if (httpRequestCachePolicy.MaxStale > TimeSpan.Zero)
+                            {
+                                cacheControl.MaxStale = true;
+                                cacheControl.MaxStaleLimit = httpRequestCachePolicy.MaxStale;
+                            }
+
+                            break;
+                        case HttpRequestCacheLevel.Refresh:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                MaxAge = TimeSpan.Zero
+                            };
+                            pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
+                            break;
+                    }
+                }
+                else
+                {
+                    switch (policy.Level)
+                    {
+                        case RequestCacheLevel.NoCacheNoStore:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                NoCache = true,
+                                NoStore = true
+                            };
+                            pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
+                            break;
+                        case RequestCacheLevel.Reload:
+                            cacheControl = new CacheControlHeaderValue
+                            {
+                                NoCache = true
+                            };
+                            pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
+                            break;
+                        case RequestCacheLevel.CacheOnly:
+                            throw new WebException(SR.CacheEntryNotFound, WebExceptionStatus.CacheEntryNotFound);
+                    }
+                }
+
+                if (cacheControl != null)
+                {
+                    request.Headers.CacheControl = cacheControl;
+                }
+            }
+        }
+
+        private RequestCachePolicy? GetApplicableCachePolicy()
+        {
+            if (CachePolicy != null)
+            {
+                return CachePolicy;
+            }
+            else if (_isDefaultCachePolicySet && DefaultCachePolicy != null)
+            {
+                return DefaultCachePolicy;
+            }
+            else
+            {
+                return WebRequest.DefaultCachePolicy;
+            }
+        }
+
         public override IAsyncResult BeginGetResponse(AsyncCallback? callback, object? state)
         {
             CheckAbort();
index d5062de..c54fe13 100644 (file)
@@ -1924,6 +1924,170 @@ namespace System.Net.Tests
             request.Abort();
         }
 
+        [Theory]
+        [InlineData(HttpRequestCacheLevel.NoCacheNoStore, null, null, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache"})]
+        [InlineData(HttpRequestCacheLevel.Reload, null, null, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
+        [InlineData(HttpRequestCacheLevel.CacheOrNextCacheOnly, null, null, new string[] { "Cache-Control: only-if-cached" })]
+        [InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MinFresh, 10, new string[] { "Cache-Control: min-fresh=10" })]
+        [InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MaxAge, 10, new string[] { "Cache-Control: max-age=10" })]
+        [InlineData(HttpRequestCacheLevel.Default, HttpCacheAgeControl.MaxStale, 10, new string[] { "Cache-Control: max-stale=10" })]
+        [InlineData(HttpRequestCacheLevel.Refresh, null, null, new string[] { "Pragma: no-cache", "Cache-Control: max-age=0" })]
+        public async Task SendHttpGetRequest_WithHttpCachePolicy_AddCacheHeaders(
+            HttpRequestCacheLevel requestCacheLevel, HttpCacheAgeControl? ageControl, int? age, string[] expectedHeaders)
+        {
+            await LoopbackServer.CreateServerAsync(async (server, uri) =>
+            {
+                HttpWebRequest request = WebRequest.CreateHttp(uri);
+                request.CachePolicy = ageControl != null ?
+                    new HttpRequestCachePolicy(ageControl.Value, TimeSpan.FromSeconds((double)age))
+                    : new HttpRequestCachePolicy(requestCacheLevel);
+                Task<WebResponse> getResponse = GetResponseAsync(request);
+
+                await server.AcceptConnectionAsync(async connection =>
+                {
+                    List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+
+                    foreach (string header in expectedHeaders)
+                    {
+                        Assert.Contains(header, headers);
+                    }
+                });
+
+                using (var response = (HttpWebResponse)await getResponse)
+                {
+                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                }
+            });
+        }
+
+        [Theory]
+        [InlineData(RequestCacheLevel.NoCacheNoStore, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache" })]
+        [InlineData(RequestCacheLevel.Reload, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
+        public async Task SendHttpGetRequest_WithCachePolicy_AddCacheHeaders(
+            RequestCacheLevel requestCacheLevel, string[] expectedHeaders)
+        {
+            await LoopbackServer.CreateServerAsync(async (server, uri) =>
+            {
+                HttpWebRequest request = WebRequest.CreateHttp(uri);
+                request.CachePolicy = new RequestCachePolicy(requestCacheLevel);
+                Task<WebResponse> getResponse = GetResponseAsync(request);
+
+                await server.AcceptConnectionAsync(async connection =>
+                {
+                    List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+
+                    foreach (string header in expectedHeaders)
+                    {
+                        Assert.Contains(header, headers);
+                    }
+                });
+
+                using (var response = (HttpWebResponse)await getResponse)
+                {
+                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                }
+            });
+        }
+
+        [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        [InlineData(RequestCacheLevel.NoCacheNoStore, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache" })]
+        [InlineData(RequestCacheLevel.Reload, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
+        public void SendHttpGetRequest_WithGlobalCachePolicy_AddCacheHeaders(
+            RequestCacheLevel requestCacheLevel, string[] expectedHeaders)
+        {
+            RemoteExecutor.Invoke(async (async, reqCacheLevel, eh0, eh1) =>
+            {
+                await LoopbackServer.CreateServerAsync(async (server, uri) =>
+                {
+                    HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(Enum.Parse<RequestCacheLevel>(reqCacheLevel));
+                    HttpWebRequest request = WebRequest.CreateHttp(uri);
+                    Task<WebResponse> getResponse = bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse());
+
+                    await server.AcceptConnectionAsync(async connection =>
+                    {
+                        List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+                        Assert.Contains(eh0, headers);
+                        Assert.Contains(eh1, headers);
+                    });
+
+                    using (var response = (HttpWebResponse)await getResponse)
+                    {
+                        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    }
+                });
+            }, (this is HttpWebRequestTest_Async).ToString(), requestCacheLevel.ToString(), expectedHeaders[0], expectedHeaders[1]).Dispose();
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task SendHttpGetRequest_WithCachePolicyCacheOnly_ThrowException(
+            bool isHttpCachePolicy)
+        {
+            HttpWebRequest request = WebRequest.CreateHttp("http://anything");
+            request.CachePolicy = isHttpCachePolicy ? new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheOnly)
+                : new RequestCachePolicy(RequestCacheLevel.CacheOnly);
+            WebException exception = await Assert.ThrowsAsync<WebException>(() => GetResponseAsync(request));
+            Assert.Equal(SR.CacheEntryNotFound, exception.Message);
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void SendHttpGetRequest_WithGlobalCachePolicyBypassCache_DoNotAddCacheHeaders()
+        {
+            RemoteExecutor.Invoke(async () =>
+            {
+                await LoopbackServer.CreateServerAsync(async (server, uri) =>
+                {
+                    HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+                    HttpWebRequest request = WebRequest.CreateHttp(uri);
+                    Task<WebResponse> getResponse = request.GetResponseAsync();
+
+                    await server.AcceptConnectionAsync(async connection =>
+                    {
+                        List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+
+                        foreach (string header in headers)
+                        {
+                            Assert.DoesNotContain("Pragma", header);
+                            Assert.DoesNotContain("Cache-Control", header);
+                        }
+                    });
+
+                    using (var response = (HttpWebResponse)await getResponse)
+                    {
+                        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    }
+                });
+            }).Dispose();
+        }
+
+        [Fact]
+        public async Task SendHttpGetRequest_WithCachePolicyBypassCache_DoNotAddHeaders()
+        {
+            await LoopbackServer.CreateServerAsync(async (server, uri) =>
+            {
+                HttpWebRequest request = WebRequest.CreateHttp(uri);
+                request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+                Task<WebResponse> getResponse = request.GetResponseAsync();
+
+                await server.AcceptConnectionAsync(async connection =>
+                {
+                    List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+
+                    foreach (string header in headers)
+                    {
+                        Assert.DoesNotContain("Pragma", header);
+                        Assert.DoesNotContain("Cache-Control", header);
+                    }
+                });
+
+                using (var response = (HttpWebResponse)await getResponse)
+                {
+                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                }
+            });
+        }
+
         private void RequestStreamCallback(IAsyncResult asynchronousResult)
         {
             RequestState state = (RequestState)asynchronousResult.AsyncState;
index a6746fc..5b640e6 100644 (file)
@@ -1,6 +1,10 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Collections.Generic;
+using System.Net.Cache;
+using System.Net.Test.Common;
+using System.Threading.Tasks;
 using Microsoft.DotNet.RemoteExecutor;
 using Xunit;
 
@@ -186,6 +190,65 @@ namespace System.Net.Tests
             Assert.False(success);
         }
 
+        [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        [InlineData(RequestCacheLevel.NoCacheNoStore, new string[] { "Pragma: no-cache", "Cache-Control: no-store, no-cache" })]
+        [InlineData(RequestCacheLevel.Reload, new string[] { "Pragma: no-cache", "Cache-Control: no-cache" })]
+        public void SendGetRequest_WithGlobalCachePolicy_AddCacheHeaders(
+            RequestCacheLevel requestCacheLevel, string[] expectedHeaders)
+        {
+            RemoteExecutor.Invoke(async (reqCacheLevel, eh0, eh1) =>
+            {
+                await LoopbackServer.CreateServerAsync(async (server, uri) =>
+                {
+                    WebRequest.DefaultCachePolicy = new RequestCachePolicy(Enum.Parse<RequestCacheLevel>(reqCacheLevel));
+                    WebRequest request = WebRequest.Create(uri);
+                    Task<WebResponse> getResponse = request.GetResponseAsync();
+
+                    await server.AcceptConnectionAsync(async connection =>
+                    {
+                        List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+                        Assert.Contains(eh0, headers);
+                        Assert.Contains(eh1, headers);
+                    });
+
+                    using (var response = (HttpWebResponse)await getResponse)
+                    {
+                        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    }
+                });
+            }, requestCacheLevel.ToString(), expectedHeaders[0], expectedHeaders[1]).Dispose();
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void SendGetRequest_WithGlobalCachePolicyBypassCache_DoNotAddCacheHeaders()
+        {
+            RemoteExecutor.Invoke(async () =>
+            {
+                await LoopbackServer.CreateServerAsync(async (server, uri) =>
+                {
+                    WebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
+                    WebRequest request = WebRequest.Create(uri);
+                    Task<WebResponse> getResponse = request.GetResponseAsync();
+
+                    await server.AcceptConnectionAsync(async connection =>
+                    {
+                        List<string> headers = await connection.ReadRequestHeaderAndSendResponseAsync();
+
+                        foreach(string header in headers)
+                        {
+                            Assert.DoesNotContain("Pragma", header);
+                            Assert.DoesNotContain("Cache-Control", header);
+                        }
+                    });
+
+                    using (var response = (HttpWebResponse)await getResponse)
+                    {
+                        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    }
+                });
+            }).Dispose();
+        }
+
         private class FakeRequest : WebRequest
         {
             private readonly Uri _uri;