Giter Club home page Giter Club logo

httpcacheheaders's Introduction

Http Cache Headers Middleware for ASP.NET Core

ASP.NET Core middleware that adds HttpCache headers to responses (Cache-Control, Expires, ETag, Last-Modified), and implements cache expiration & validation models. It can be used to ensure caches correctly cache responses and/or to implement concurrency for REST-based APIs using ETags.

The middleware itself does not store responses. Looking at this description, this middleware handles the "backend"-part: it generates the correct cache-related headers, and ensures a cache can check for expiration (304 Not Modified) & preconditions (412 Precondition Failed) (often used for concurrency checks).

It can be used together with a shared cache, a private cache or both. For production scenarios the best approach is to use this middleware to generate the ETags, combined with a cache server or CDN to inspect those tags and effectively cache the responses. In the sample, the Microsoft.AspNetCore.ResponseCaching cache store is used to cache the responses.

NuGet version

Installation (NuGet)

Install-Package Marvin.Cache.Headers

Usage

First, register the services with ASP.NET Core's dependency injection container (in the ConfigureServices method on the Startup class)

services.AddHttpCacheHeaders();

Then, add the middleware to the request pipeline. Starting with version 6.0, the middleware MUST be added between UseRouting() and UseEndpoints().

app.UseRouting(); 

app.UseHttpCacheHeaders();

app.UseEndpoints(...);

Configuring Options

The middleware allows customization of how headers are generated. The AddHttpCacheHeaders() method has parameters for configuring options related to expiration, validation and middleware.

For example, this code will set the max-age directive to 600 seconds, add the must-revalidate directive and ignore header generation for all responses with status code 500.

services.AddHttpCacheHeaders(
    expirationModelOptions =>
    {
        expirationModelOptions.MaxAge = 600;
    },
    validationModelOptions =>
    {
        validationModelOptions.MustRevalidate = true;
    },
    middlewareOptions => 
    {
        middlewareOptions.IgnoreStatusCodes = new[] { 500 };
    });

There are some predefined collections with status codes you can use when you want to ignore:

  • all server errors HttpStatusCodes.ServerErrors
  • all client errors HttpStatusCodes.ClientErrors
  • all errors HttpStatusCodes.AllErrors

Action (Resource) and Controller-level Header Configuration

For anything but the simplest of cases having one global cache policy isn't sufficient: configuration at level of each resource (action/controller) is required. For those cases, use the HttpCacheExpiration and/or HttpCacheValidation attributes at action or controller level.

[HttpGet]
[HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 99999)]
[HttpCacheValidation(MustRevalidate = true)]
public IEnumerable<string> Get()
{
    return new[] { "value1", "value2" };
}
```
Both override the global options.  Action-level configuration overrides controller-level configuration.

# Ignoring Cache Headers / eTag Generation

You don't always want tags / headers to be generated for all resources (e.g.: for a large file).  You can ignore generation by applying the HttpCacheIgnore attribute at controller or action level. 

````csharp
[HttpGet]
[HttpCacheIgnore]
public IEnumerable<string> Get()
{
    return new[] { "value1", "value2" };
}

If you want to globally disable automatic header generation, you can do so by setting DisableGlobalHeaderGeneration on the middleware options to true.

services.AddHttpCacheHeaders(     
    middlewareOptionsAction: middlewareOptions => 
    {
        middlewareOptions.DisableGlobalHeaderGeneration = true;
    });

Marking for Invalidation

Cache invalidation essentially means wiping a response from the cache because you know it isn't the correct version anymore. Caches often partially automate this (a response can be invalidated when it becomes stale, for example) and/or expose an API to manually invalidate items.

The same goes for the cache headers middleware, which holds a store of records with previously generated cache headers & tags. Replacement of store key records (/invalidation) is mostly automatic. Say you're interacting with values/1. First time the backend is hit and you get back an eTag in the response headers. Next request you send is again a GET request with the "If-None-Match"-header set to the eTag: the backend won't be hit. Then, you send a PUT request to values/1, which potentially results in a change; if you send a GET request now, the backend will be hit again.

However: if you're updating/changing resources by using an out of band mechanism (eg: a backend process that changes the data in your database, or a resource gets updated that has an update of related resources as a side effect), this process can't be automated.

Take a list of employees as an example. If a PUT statement is sent to one "employees" resource, then that one "employees" resource will get a new Etag. Yet: if you're sending a PUT request to one specific employee ("employees/1", "employees/2", ...), this might have the effect that the "employees" resource has also changed: if the employee you just updated is one of the employees in the returned employees list when fetching the "employees" resource, the "employees" resource is out of date. Same goes for deleting or creating an employee: that, too, might have an effect on the "employees" resource.

To support this scenario the cache headers middleware allows marking an item for invalidation. When doing that, the related item will be removed from the internal store, meaning that for subsequent requests a stored item will not be found.

To use this, inject an IValidatorValueInvalidator and call MarkForInvalidation on it, passing through the key(s) of the item(s) you want to be removed. You can additionally inject an IStoreKeyAccessor, which contains methods that make it easy to find one or more keys from (part of) a URI.

Extensibility

The middleware is very extensible. If you have a look at the AddHttpCacheHeaders method you'll notice it allows injecting custom implementations of IValidatorValueStore, IStoreKeyGenerator, IETagGenerator and/or IDateParser (via actions).

IValidatorValueStore

A validator value store stores validator values. A validator value is used by the cache validation model when checking if a cached item is still valid. It contains ETag and LastModified properties. The default IValidatorValueStore implementation (InMemoryValidatorValueStore) is an in-memory store that stores items in a ConcurrentDictionary<string, ValidatorValue>.

/// <summary>
/// Contract for a store for validator values.  Each item is stored with a <see cref="StoreKey" /> as key```
/// and a <see cref="ValidatorValue" /> as value (consisting of an ETag and Last-Modified date).   
/// </summary>
public interface IValidatorValueStore
{
    /// <summary>
    /// Get a value from the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to get.</param>
    /// <returns></returns>
    Task<ValidatorValue> GetAsync(StoreKey key);
    /// <summary>
    /// Set a value in the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to store.</param>
    /// <param name="validatorValue">The <see cref="ValidatorValue"/> to store.</param>
    /// <returns></returns>
    Task SetAsync(StoreKey key, ValidatorValue validatorValue);

    /// <summary>
    /// Find one or more keys that contain the inputted valueToMatch 
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <param name="ignoreCase">Ignore case when matching</param>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindStoreKeysByKeyPartAsync(string valueToMatch, bool ignoreCase);
}

BREAKING CHANGE from v7 onwards: the FindStoreKeysByKeyPartAsync methods return an IAsyncEnumerable to enable async streaming of results.

/// <summary>
/// Contract for a store for validator values.  Each item is stored with a <see cref="StoreKey" /> as key```
/// and a <see cref="ValidatorValue" /> as value (consisting of an ETag and Last-Modified date).   
/// </summary>
public interface IValidatorValueStore
{
    /// <summary>
    /// Get a value from the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to get.</param>
    /// <returns></returns>
    Task<ValidatorValue> GetAsync(StoreKey key);
    /// <summary>
    /// Set a value in the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to store.</param>
    /// <param name="validatorValue">The <see cref="ValidatorValue"/> to store.</param>
    /// <returns></returns>
    Task SetAsync(StoreKey key, ValidatorValue validatorValue);

    /// <summary>
    /// Find one or more keys that contain the inputted valueToMatch 
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <param name="ignoreCase">Ignore case when matching</param>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindStoreKeysByKeyPartAsync(string valueToMatch, bool ignoreCase);
}

IStoreKeyGenerator

The StoreKey, as used by the IValidatorValueStore as key, can be customized as well. To do so, implement the IStoreKeyGenerator interface. The default implementation (DefaultStoreKeyGenerator) generates a key from the request path, request query string and request header values (taking VaryBy into account). Through StoreKeyContext you can access all applicable values that can be useful for generating such a key.

/// <summary>
/// Contract for a key generator, used to generate a <see cref="StoreKey" /> ```
/// </summary>
public interface IStoreKeyGenerator
{
    /// <summary>
    /// Generate a key for storing a <see cref="ValidatorValue"/> in a <see cref="IValidatorValueStore"/>.
    /// </summary>
    /// <param name="context">The <see cref="StoreKeyContext"/>.</param>         
    /// <returns></returns>
    Task<StoreKey> GenerateStoreKey(
        StoreKeyContext context);
}

IETagGenerator

You can inject an IETagGenerator-implementing class to modify how ETags are generated (ETags are part of a ValidatorValue). The default implementation (DefaultStrongETagGenerator) generates strong Etags from the request key + response body (MD5 hash from combined bytes).

/// <summary>
/// Contract for an E-Tag Generator, used to generate the unique weak or strong E-Tags for cache items
/// </summary>
public interface IETagGenerator
{
    Task<ETag> GenerateETag(
        StoreKey storeKey,
        string responseBodyContent);
}

IETagInjector

You can inject an IETagInjector-implementing class to modify how, where and when ETags are provided. The default implementation (DefaultETagInjector) injects the DefaultETag generator using the response body on the http context as the string along with the provided request key.

/// <summary>
///     Contract for a ETagInjector, which can be used to inject custom eTags for resources
///     of which may be injected in the request pipeline (eg: based on existing calculated eTag on resource and store key)
/// </summary>
/// <remarks>
///     This injector will wrap the <see cref="IETagGenerator" /> to allow for eTag source to be swapped out
///     based on the <see cref="HttpContext" /> (rather than extend the interface of <see cref="IETagInjector" /> to
///     to extended including the <see cref="HttpContext" />
/// </remarks>
public interface IETagInjector
{
    Task<ETag> RetrieveETag(ETagContext eTagContext);
}

ILastModifiedInjector

You can inject an ILastModifiedInjector-implementing class to modify how LastModified values are provided. The default implementation (DefaultLastModifiedInjector) injects the current UTC.

/// <summary>
/// Contract for a LastModifiedInjector, which can be used to inject custom last modified dates for resources
/// of which you know when they were last modified (eg: a DB timestamp, custom logic, ...)
/// </summary>
public interface ILastModifiedInjector
{
    Task<DateTimeOffset> CalculateLastModified(
        ResourceContext context);
}

IDateParser

Through IDateParser you can inject a custom date parser in case you want to override the default way dates are stringified. The default implementation (DefaultDateParser) uses the RFC1123 pattern (https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx).

/// <summary>
/// Contract for a date parser, used to parse Last-Modified, Expires, If-Modified-Since and If-Unmodified-Since headers.
/// </summary>
public interface IDateParser
{
    Task<string> LastModifiedToString(DateTimeOffset lastModified);

    Task<string> ExpiresToString(DateTimeOffset lastModified);

    Task<DateTimeOffset?> IfModifiedSinceToDateTimeOffset(string ifModifiedSince);

    Task<DateTimeOffset?> IfUnmodifiedSinceToDateTimeOffset(string ifUnmodifiedSince);
}

IValidatorValueInvalidator

An IValidatorValueInvalidator-implenting class is responsible for marking items for invalidation.

/// <summary>
/// Contract for the <see cref="ValidatorValueInvalidator" />
/// </summary>
public interface IValidatorValueInvalidator
{
    /// <summary>
    /// Get the list of <see cref="StoreKey" /> of items marked for invalidation
    /// </summary>
    List<StoreKey> KeysMarkedForInvalidation { get; }

    /// <summary>
    /// Mark an item stored with a <see cref="StoreKey" /> for invalidation
    /// </summary>
    /// <param name="storeKey">The <see cref="StoreKey" /></param>
    /// <returns></returns>
    Task MarkForInvalidation(StoreKey storeKey);

    /// <summary>
    /// Mark a set of items for invlidation by their collection of <see cref="StoreKey" /> 
    /// </summary>
    /// <param name="storeKeys">The collection of <see cref="StoreKey" /></param>
    /// <returns></returns>
    Task MarkForInvalidation(IEnumerable<StoreKey> storeKeys);
}

IStoreKeyAccessor

The IStoreKeyAccessor contains helper methods for getting keys from parts of a URI. Override this if you're not storing items with their default keys.

/// <summary>
/// Contract for finding (a) <see cref="StoreKey" />(s)
/// </summary>    
public interface IStoreKeyAccessor
{
    /// <summary>
    /// Find a  <see cref="StoreKey" /> by part of the key
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindByKeyPart(string valueToMatch);

    /// <summary>
    /// Find a  <see cref="StoreKey" /> of which the current resource path is part of the key
    /// </summary>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindByCurrentResourcePath();
}

BREAKING CHANGE from v7 onwards: the methods return an IAsyncEnumerable to enable async streaming of results.

/// <summary>
/// Contract for finding (a) <see cref="StoreKey" />(s)
/// </summary>    
public interface IStoreKeyAccessor
{
    /// <summary>
    /// Find a  <see cref="StoreKey" /> by part of the key
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindByKeyPart(string valueToMatch);

    /// <summary>
    /// Find a  <see cref="StoreKey" /> of which the current resource path is part of the key
    /// </summary>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindByCurrentResourcePath();
}

httpcacheheaders's People

Contributors

clawrenceks avatar cumpsd avatar derstimmler avatar jeffward01 avatar kevindockx avatar maartenba avatar payini avatar ryan-h avatar seanfarrow avatar shawnwildermuth avatar tomseida avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

httpcacheheaders's Issues

Upgrade to AspNetCore 2.1

I have just upgraded a fork to 2.1.1. It wasn't straightforward. Included is the diff.

It now builds against

$ dotnet --version
2.1.301

Some gotchas:

  • Renamed package: Microsoft.AspNetCore.All --> Microsoft.AspNetCore.App
  • Drop version for above package, except in the Test project (I can't explain why)
  • Manually change Target Frameworks through text editing file from 2.0 --> 2.1
Index: test/Marvin.Cache.Headers.Test/Marvin.Cache.Headers.Test.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- test/Marvin.Cache.Headers.Test/Marvin.Cache.Headers.Test.csproj	(revision 99b980343d3d2b54b29208f8f54ab5b8e5a3f976)
+++ test/Marvin.Cache.Headers.Test/Marvin.Cache.Headers.Test.csproj	(date 1529885235000)
@@ -1,28 +1,25 @@
 <Project Sdk="Microsoft.NET.Sdk">
-
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
     <AssemblyName>Marvin.Cache.Headers.Test</AssemblyName>
     <PackageId>Marvin.Cache.Headers.Test</PackageId>
     <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
   </PropertyGroup>
-
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0"></PackageReference>
-    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
+    <PackageReference Include="Microsoft.AspNetCore.App"  Version="2.1.1">
+    </PackageReference>
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
+    <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
-    <PackageReference Include="Moq" Version="4.7.142" />
+    <PackageReference Include="Moq" Version="4.8.3" />
     <PackageReference Include="xunit" Version="2.3.1" />
   </ItemGroup>
-
   <ItemGroup>
     <Folder Include="Properties\" />
   </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\..\sample\Marvin.Cache.Headers.Sample\Marvin.Cache.Headers.Sample.csproj" />
     <ProjectReference Include="..\..\src\Marvin.Cache.Headers\Marvin.Cache.Headers.csproj" />
   </ItemGroup>
-
-</Project>
+</Project>
\ No newline at end of file
Index: sample/Marvin.Cache.Headers.Sample/Marvin.Cache.Headers.Sample.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- sample/Marvin.Cache.Headers.Sample/Marvin.Cache.Headers.Sample.csproj	(revision 99b980343d3d2b54b29208f8f54ab5b8e5a3f976)
+++ sample/Marvin.Cache.Headers.Sample/Marvin.Cache.Headers.Sample.csproj	(date 1529884438000)
@@ -1,21 +1,17 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
-
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
   </PropertyGroup>
-
   <ItemGroup>
     <None Update="wwwroot\**\*">
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
     </None>
   </ItemGroup>
-
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0"></PackageReference>
+    <PackageReference Include="Microsoft.AspNetCore.App">
+    </PackageReference>
   </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\..\src\Marvin.Cache.Headers\Marvin.Cache.Headers.csproj" />
   </ItemGroup>
-
-</Project>
+</Project>
\ No newline at end of file
Index: src/Marvin.Cache.Headers/Marvin.Cache.Headers.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/Marvin.Cache.Headers/Marvin.Cache.Headers.csproj	(revision 99b980343d3d2b54b29208f8f54ab5b8e5a3f976)
+++ src/Marvin.Cache.Headers/Marvin.Cache.Headers.csproj	(date 1529882703000)
@@ -1,25 +1,22 @@
 <Project Sdk="Microsoft.NET.Sdk">
-
   <PropertyGroup>
-    <TargetFramework>netstandard1.6</TargetFramework>
+    <TargetFramework>netstandard2.0</TargetFramework>
     <AssemblyName>Marvin.Cache.Headers</AssemblyName>
     <PackageId>Marvin.Cache.Headers</PackageId>
-    <NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
+    <NetStandardImplicitPackageVersion>2.0.3</NetStandardImplicitPackageVersion>
     <PackageTargetFallback>$(PackageTargetFallback);dnxcore50</PackageTargetFallback>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
     <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
     <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
-    <Version>1.1.0</Version>
+    <Version>2.1.1</Version>
     <Description>ASP.NET Core middleware that adds HttpCache headers to responses (Cache-Control, Expires, ETag, Last-Modified), and implements cache expiration &amp; validation models.</Description>
   </PropertyGroup>
-
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="1.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="1.0.2" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.0.2" />
-    <PackageReference Include="Microsoft.Net.Http.Headers" Version="1.0.2" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.0.2" />
-    <PackageReference Include="Microsoft.Extensions.Options" Version="1.0.2" />
+    <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
+    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
+    <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.1.1" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
+    <PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
   </ItemGroup>
-
-</Project>
+</Project>
\ No newline at end of file

Creating a response in app.UseExceptionHandler is throwing an error

Given the following exception handler
app.UseExceptionHandler(builder => { builder.Run(async context => { context.Response.StatusCode = 500; await context.Response.WriteAsync("An unexpected fault happened. Try again later"); }); });

If I'm using app.UseHttpCacheHeaders(); and i throw an un-handled exception I'll get the following exception
"exception": { "type": "ObjectDisposedException", "message": "Cannot access a closed Stream.", "method": "System.IO.__Error.StreamIsClosed()", "stacktrace": " at System.IO.__Error.StreamIsClosed()\r\n at System.IO.MemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count)\r\n at System.IO.MemoryStream.WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n at WebApiPattern.AspCore.Startup.<>c.<<Configure>b__1_1>d.MoveNext() in C:\\Workspace\\webapi_pattern\\WebApiPattern.AspCore\\Startup.cs:line 148\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>d__6.MoveNext()", "innerException": {}

Also I've been going through your PluralSight course and its great!

No Content (204) responses are causing exceptions

I'm using NLog to log exceptions and I've noticed that whenever a controller returns a No Content (204), the following is getting logged.

"exception": {
		"type": "InvalidOperationException",
		"message": "Write to non-body 204 response.",
		"method": "Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.HandleNonBodyResponseWrite()",
		"stacktrace": "   at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.HandleNonBodyResponseWrite()\r\n   at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.<WriteAsyncAwaited>d__183.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Marvin.Cache.Headers.HttpCacheHeadersMiddleware.<Invoke>d__6.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware.<Invoke>d__8.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.AspNetCore.Hosting.Internal.RequestServicesContainerMiddleware.<Invoke>d__3.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame`1.<RequestProcessingAsync>d__2.MoveNext()",
		"innerException": {}

Commenting out the app.UseHttpCacheHeaders(); from Startup removes the error.

Allow configuring that the eTags generated should be considered weak eTags.

This is useful when the server wants to input his own strong eTag-generating function. This use case is valid if the default function doesn't guarantee equality according to the server, or the other way around: if the generation of a strong eTag should not take into account the full response body. Eg: a modifiedby-field shouldn't be taken into account, even with different modifiedby-fields, responses are still regarded as equal In these cases, the user must provide his own strong eTag-generating functions.

app.UseDeveloperExceptionPage() issue

Hi there,

I found that if I have an error in my application and I use the following order in startup:

app.UseDeveloperExceptionPage(); 
app.UseHttpCacheHeaders();
app.UseMvc();

I get a blank page instead of the exception details in the page.

If I use the following order then everything works correctly:

app.UseHttpCacheHeaders();
app.UseDeveloperExceptionPage(); 
app.UseMvc();

Could you please clarify the ordering needed, or perhaps your middleware needs to be bypassed when an error occurs?

Provide an option to include Access-Control-Expose-Headers in the response header

Although the ETag header is included in the response, it cannot be accessed thru JavaScript when hosted from a different domain (foreign origin). According to a post in StackOverflow located at https://stackoverflow.com/a/25673446/4997224, there is a need to include the Access-Control-Expose-Headers to give explicit permissions for the client to read headers like the ETag header.

Here's what I did in my local copy of the HttpCacheHeaders. I added the last line of code after the code that sets/adds the ETag header and it seems to solve my problem. Unfortunately, the Access-Control-Expose-Headers string is not defined in the HeaderNames class so I had to hard-code it here.

// set the ETag header
headers[HeaderNames.ETag] = eTag.Value;
// expose ETag header over CORS
headers["Access-Control-Expose-Headers"] = "etag";

It would be nice if we can make this configurable.

Allow customer Last-Modified

I have resources that I know the last modified (which is not the time of generation) and I want to return this.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified

The Last-Modified response HTTP header contains the date and time at which the origin server believes the resource was last modified. It is used as a validator to determine if a resource received or stored is the same. Less accurate than an ETag header, it is a fallback mechanism. Conditional requests containing If-Modified-Since or If-Unmodified-Since headers make use of this field.

304 & 412 response headers: wrong ETag

Currently, 304 responses & 412 responses take the ETag headers that are included in the request. This results in a possible list of ETags when, for example, an If-None-Match header with multiple ETags is submitted. Should change to a calculated ETag, or maybe the current one from the stored response (check standard to be sure).

Support multiple values for If-None-Match fields instead of one

The If-None-Match request-header field is used with a method to make it conditional. A client that has one or more entities previously obtained from the resource can verify that none of those entities is current by including a list of their associated entity tags in the If-None-Match header field. The purpose of this feature is to allow efficient updates of cached information with a minimum amount of transaction overhead. It is also used to prevent a method (e.g. PUT) from inadvertently modifying an existing resource when the client believes that the resource does not exist.

Examples:
If-None-Match: "xyzzy"
If-None-Match: W/"xyzzy"
If-None-Match: "xyzzy", "r2d2xxxx", "c3piozzzz"
If-None-Match: W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"
If-None-Match: *

Cfr: https://tools.ietf.org/html/rfc2616#page-132

Create sample with ResponseCaching middleware as a cache store

Add an example with a shared cache store. Ideal candidate (safe for the naming...) seems to be Microsoft's Response Caching middleware. However, at this time the current version doesn't look stable enough (no-cache issues, cache invalidation issues, revalidation issues, ...). Once it's finished (1.2.0?), add a sample so responses actually get cached :)

Large files

Hello,

It would be nice to have an option to not generate ETag header for large files because ReadToEnd can be long in this case.

Support If-Match *

The meaning of "If-Match: *" is that the method SHOULD be performed if the representation selected by the origin server (or by a cache, possibly using the Vary mechanism) exists, and MUST NOT be performed if the representation does not exist.

Cfr: https://tools.ietf.org/html/rfc2616#page-128

Support multiple values for If-Match fields instead of one

A request intended to update a resource (e.g., a PUT) MAY include an If-Match header field to signal that the request method MUST NOT be applied if the entity corresponding to the If-Match value (a single entity tag) is no longer a representation of that resource. This allows the user to indicate that they do not wish the request to be successful if the resource has been changed without their knowledge.

Examples:
If-Match: "xyzzy"
If-Match: "xyzzy", "r2d2xxxx", "c3piozzzz"
If-Match: *

Cfr: https://tools.ietf.org/html/rfc2616#page-128

Question: Why update Last-Modified when found in store?

https://github.com/KevinDockx/HttpCacheHeaders/blob/master/src/Marvin.Cache.Headers/HttpCacheHeadersMiddleware.cs#L85

I am trying to understand, why are we updating the Last Modified in the store? Shouldn't we let the store decide this?

Given an empty store and an initial request:

  • Checks occur for caching
  • None match, request goes as normal, response is built
  • ETag is calculated and last modified is UtcNow
  • This gets stored in the store and the response is sent

Given previous with a request for the same

  • Checks for cache occur
  • Found in store
  • Update Last Modified in store
  • Send back 302 not modified with a different last modified

Why are we doing the update? Image I have a redis store, and I want the last modified date be updated by another process, which knows a lot better when the object itself has changed (smarter cache invalidation), this would get overwritten now. Does that make sense?

Support If-None-Match *

The meaning of "If-None-Match: *" is that the method MUST NOT be performed if the representation selected by the origin server (or by a cache, possibly using the Vary mechanism) exists, and SHOULD be performed if the representation does not exist. This feature is intended to be useful in preventing races between PUT operations.

Cfr: https://tools.ietf.org/html/rfc2616#page-132

ETags & concurrency checks in combination with MS's cache store

(cfr course comment: "However, when I tried following your instructions in the Dealing with Concurrency demo, after performing the PUT operation, it failed to update the ETag and retained the same ETag as in the two GET Operations.") => so the ETag isn't updated, which makes sense b/c MS's middleware doesn't generate ETags.

=> works when not using the response caching cache store middleware
=> works when using the response caching cache store middleware & sending a no-cache (so the store is bypassed)
=> doesn't work when using the middleware combined with marvin.cacheheaders: ETag isn't updated.

MS's cache store doesn't generate ETags, but it seems like it doesn't pass through the request either... should such requests always be sent with no-cache? Is this an issue with MS's cache store? Is it an issue with this middleware? To investigate.

Support If-Unmodified-Since for validation model (conditional updates)

From the standard:

The If-Unmodified-Since request-header field is used with a method to make it conditional. If the requested resource has not been modified since the time specified in this field, the server SHOULD perform the requested operation as if the If-Unmodified-Since header were not present.

If the requested variant has been modified since the specified time, the server MUST NOT perform the requested operation, and MUST return a 412 (Precondition Failed).

  If-Unmodified-Since = "If-Unmodified-Since" ":" HTTP-date

An example of the field is:

   If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT

If the request normally (i.e., without the If-Unmodified-Since header) would result in anything other than a 2xx or 412 status, the If-Unmodified-Since header SHOULD be ignored.

If the specified date is invalid, the header is ignored.

The result of a request having both an If-Unmodified-Since header field and either an If-None-Match or an If-Modified-Since header fields is undefined by this specification.

To check: shouldn't this only be used if the server can ensure those dates are strong validators? Might need additional design so users can signify this.

cache by query string

I've followed your course on Pluralsight and implemented this middleware in my application as follows:

services.AddHttpCacheHeaders( expirationModelOptions => { expirationModelOptions.MaxAge = 600; expirationModelOptions.SharedMaxAge = 300; }, validationModelOptions => { validationModelOptions.AddMustRevalidate = true; validationModelOptions.AddProxyRevalidate = true; });

However, I've got a sort, filter, pagination set up in my query string like: /api/objects?filter.name='a'
When I call the same url but with another querystring, for example to return all objects with a 'b' in their name : /api/objects/filter.name='b', I receive a '304 not modified'.

What can I do to get the urls with the filter query string revalidate if the query string changed?

Question different cache authenticated vs non authenticated

Firstly great effort with the project.

I was wondering how would you handle the situation where you would like to cache non authenticated pages, but if a person logins then don't cache the page?

Is a validation attribute the way to go?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.