Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request CachePolicy isn't applied in HTTP request header #60913

Merged
merged 7 commits into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
scalablecory marked this conversation as resolved.
Show resolved Hide resolved
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