diff --git a/src/libraries/System.Net.Requests/src/Resources/Strings.resx b/src/libraries/System.Net.Requests/src/Resources/Strings.resx index 5240ec2908eec..a3b2bb922d3c7 100644 --- a/src/libraries/System.Net.Requests/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Requests/src/Resources/Strings.resx @@ -270,4 +270,7 @@ System.Net.Requests is not supported on this platform. + + The request was aborted: The request cache-only policy does not allow a network request and the response is not found in cache. + diff --git a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs index 3adae087059e3..c92ea9f28d5d2 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs @@ -7,6 +7,7 @@ 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 @@ public static int DefaultMaximumErrorResponseLength 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 @@ private async Task SendRequest(bool async) 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 @@ private async Task SendRequest(bool async) } } + private void AddCacheControlHeaders(HttpRequestMessage request) + { + RequestCachePolicy? policy = GetApplicableCachePolicy(); + + if (policy != null && policy.Level != RequestCacheLevel.BypassCache) + { + CacheControlHeaderValue? cacheControl = null; + HttpHeaderValueCollection 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(); diff --git a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs index d5062de525162..c54fe136cef07 100644 --- a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs @@ -1924,6 +1924,170 @@ public void Abort_CreateRequestThenAbort_Success(Uri remoteServer) 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 getResponse = GetResponseAsync(request); + + await server.AcceptConnectionAsync(async connection => + { + List 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 getResponse = GetResponseAsync(request); + + await server.AcceptConnectionAsync(async connection => + { + List 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(reqCacheLevel)); + HttpWebRequest request = WebRequest.CreateHttp(uri); + Task getResponse = bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse()); + + await server.AcceptConnectionAsync(async connection => + { + List 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(() => 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 getResponse = request.GetResponseAsync(); + + await server.AcceptConnectionAsync(async connection => + { + List 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 getResponse = request.GetResponseAsync(); + + await server.AcceptConnectionAsync(async connection => + { + List 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; diff --git a/src/libraries/System.Net.Requests/tests/WebRequestTest.cs b/src/libraries/System.Net.Requests/tests/WebRequestTest.cs index a6746fc33f219..5b640e65bdfc9 100644 --- a/src/libraries/System.Net.Requests/tests/WebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/WebRequestTest.cs @@ -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 @@ public void RegisterPrefix_DuplicateHttpWithFakeFactory_ExpectFalse() 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(reqCacheLevel)); + WebRequest request = WebRequest.Create(uri); + Task getResponse = request.GetResponseAsync(); + + await server.AcceptConnectionAsync(async connection => + { + List 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 getResponse = request.GetResponseAsync(); + + await server.AcceptConnectionAsync(async connection => + { + List 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;