Skip to content

Commit

Permalink
Implement WebRequest CachePolicy (#60913)
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobsaila committed Nov 9, 2021
1 parent 731364d commit 26c045b
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Requests/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,7 @@
<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>
131 changes: 130 additions & 1 deletion src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -1137,6 +1152,8 @@ private async Task<WebResponse> 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)
Expand Down Expand Up @@ -1202,6 +1219,118 @@ private async Task<WebResponse> SendRequest(bool async)
}
}

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();
Expand Down
164 changes: 164 additions & 0 deletions src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
Expand Down
Loading

0 comments on commit 26c045b

Please sign in to comment.