From 367e5c0a4e9b2e105d13fb01b0fc0f46d2c55feb Mon Sep 17 00:00:00 2001 From: Michael Tanczos Date: Mon, 2 Oct 2023 12:45:47 -0400 Subject: [PATCH 1/3] Adding base policy builder extension --- src/Htmx/HtmxCorsPolicyBuilderExtensions.cs | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Htmx/HtmxCorsPolicyBuilderExtensions.cs diff --git a/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs b/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs new file mode 100644 index 0000000..1bd1ff2 --- /dev/null +++ b/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Cors.Infrastructure; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; + +namespace Htmx; + +public static class HtmxCorsPolicyBuilderExtensions +{ + /// + /// Adds Htmx request headers to the policy. + /// + /// + /// + /// The current policy builder. + public static CorsPolicyBuilder WithHtmxHeaders(this CorsPolicyBuilder policyBuilder, params string[] excludeHeaders) + { + IEnumerable headers = new List () + { + HtmxRequestHeaders.Keys.CurrentUrl, + HtmxRequestHeaders.Keys.HistoryRestoreRequest, + HtmxRequestHeaders.Keys.Prompt, + HtmxRequestHeaders.Keys.Request, + HtmxRequestHeaders.Keys.Target, + HtmxRequestHeaders.Keys.TriggerName, + HtmxRequestHeaders.Keys.Trigger, + HtmxRequestHeaders.Keys.Boosted + }; + + if (excludeHeaders.Length > 0) + { + headers = headers.Except(excludeHeaders); + } + + policyBuilder.WithHeaders(headers.ToArray()); + + return policyBuilder; + } + + /// + /// Adds Htmx response headers to the policy. + /// + /// The current policy builder. + public static CorsPolicyBuilder WithExposedHtmxHeaders(this CorsPolicyBuilder policyBuilder, params string[] excludeHeaders) + { + IEnumerable headers = new List() + { + HtmxResponseHeaders.Keys.PushUrl, + HtmxResponseHeaders.Keys.Location, + HtmxResponseHeaders.Keys.Redirect, + HtmxResponseHeaders.Keys.Refresh, + HtmxResponseHeaders.Keys.Trigger, + HtmxResponseHeaders.Keys.TriggerAfterSettle, + HtmxResponseHeaders.Keys.TriggerAfterSwap, + HtmxResponseHeaders.Keys.Reswap, + HtmxResponseHeaders.Keys.Retarget, + HtmxResponseHeaders.Keys.ReplaceUrl + }; + + if (excludeHeaders.Length > 0) + { + headers = headers.Except(excludeHeaders); + } + + policyBuilder.WithExposedHeaders(headers.ToArray()); + + return policyBuilder; + } +} From e007690ed5dbd1fccd3b0b80f9fec0b281e59a0b Mon Sep 17 00:00:00 2001 From: Michael Tanczos Date: Tue, 3 Oct 2023 08:35:31 -0400 Subject: [PATCH 2/3] Removed policy builder, added All to response/request classes, and updated docs Updated docs Updated docs --- Readme.md | 42 ++++++++++++ src/Htmx/HtmxCorsPolicyBuilderExtensions.cs | 72 --------------------- src/Htmx/HtmxRequestHeaders.cs | 5 ++ src/Htmx/HtmxResponseHeaders.cs | 5 ++ 4 files changed, 52 insertions(+), 72 deletions(-) delete mode 100644 src/Htmx/HtmxCorsPolicyBuilderExtensions.cs diff --git a/Readme.md b/Readme.md index 6552274..ca37040 100644 --- a/Readme.md +++ b/Readme.md @@ -39,6 +39,22 @@ Request.IsHtmx(out var values); Read more about the other header values on the [official documentation page](https://htmx.org/reference/#request_headers). +#### Browser Caching + +As a special note, please be mindful that if your server can render different content for the same URL depending on some other headers, you need to use the Vary response HTTP header. For example, if your server renders the full HTML when Request.IsHtmx() is false, and it renders a fragment of that HTML when Request.IsHtmx() is true, you need to add Vary: HX-Request. That causes the cache to be keyed based on a composite of the response URL and the HX-Request request header — rather than being based just on the response URL. + +```c# +// in a Razor Page +if (Request.IsHtmx()) +{ + Response.Headers.Add("Vary", "HX-Request"); + return Partial("_Form", this) +} + +return Page(); +``` + + ### HttpResponse We can set Http Response headers using the `Htmx` extension method, which passes an action and `HtmxResponseHeaders` object. @@ -64,6 +80,32 @@ Response.Htmx(h => { }); ``` +#### CORS Policy + +By default, all Htmx requests and responses will be blocked by the browser in a cross-origin context. + +If you configure your application in a cross-origin context, then setting a CORS policy in ASP.NET Core also allows you to define specific restrictions on request and response headers, +enabling fine-grained control over the data that can be exchanged between your web application and different origins. + +This library provides a simple approach to exposing Htmx headers to your CORS policy: + +```c# +var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => +{ + options.AddPolicy(name: MyAllowSpecificOrigins, + policy => + { + policy.WithOrigins("http://example.com", "http://www.contoso.com") + .WithHeaders(HtmxRequestHeaders.Keys.All) // Add htmx request headers + .WithExposedHeaders(HtmxResponseHeaders.Keys.All) // Add htmx response headers + }); +}); +``` + ## Htmx.TagHelpers ### Getting Started diff --git a/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs b/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs deleted file mode 100644 index 1bd1ff2..0000000 --- a/src/Htmx/HtmxCorsPolicyBuilderExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.AspNetCore.Cors.Infrastructure; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; - -namespace Htmx; - -public static class HtmxCorsPolicyBuilderExtensions -{ - /// - /// Adds Htmx request headers to the policy. - /// - /// - /// - /// The current policy builder. - public static CorsPolicyBuilder WithHtmxHeaders(this CorsPolicyBuilder policyBuilder, params string[] excludeHeaders) - { - IEnumerable headers = new List () - { - HtmxRequestHeaders.Keys.CurrentUrl, - HtmxRequestHeaders.Keys.HistoryRestoreRequest, - HtmxRequestHeaders.Keys.Prompt, - HtmxRequestHeaders.Keys.Request, - HtmxRequestHeaders.Keys.Target, - HtmxRequestHeaders.Keys.TriggerName, - HtmxRequestHeaders.Keys.Trigger, - HtmxRequestHeaders.Keys.Boosted - }; - - if (excludeHeaders.Length > 0) - { - headers = headers.Except(excludeHeaders); - } - - policyBuilder.WithHeaders(headers.ToArray()); - - return policyBuilder; - } - - /// - /// Adds Htmx response headers to the policy. - /// - /// The current policy builder. - public static CorsPolicyBuilder WithExposedHtmxHeaders(this CorsPolicyBuilder policyBuilder, params string[] excludeHeaders) - { - IEnumerable headers = new List() - { - HtmxResponseHeaders.Keys.PushUrl, - HtmxResponseHeaders.Keys.Location, - HtmxResponseHeaders.Keys.Redirect, - HtmxResponseHeaders.Keys.Refresh, - HtmxResponseHeaders.Keys.Trigger, - HtmxResponseHeaders.Keys.TriggerAfterSettle, - HtmxResponseHeaders.Keys.TriggerAfterSwap, - HtmxResponseHeaders.Keys.Reswap, - HtmxResponseHeaders.Keys.Retarget, - HtmxResponseHeaders.Keys.ReplaceUrl - }; - - if (excludeHeaders.Length > 0) - { - headers = headers.Except(excludeHeaders); - } - - policyBuilder.WithExposedHeaders(headers.ToArray()); - - return policyBuilder; - } -} diff --git a/src/Htmx/HtmxRequestHeaders.cs b/src/Htmx/HtmxRequestHeaders.cs index 3cfb238..ee42258 100644 --- a/src/Htmx/HtmxRequestHeaders.cs +++ b/src/Htmx/HtmxRequestHeaders.cs @@ -20,6 +20,11 @@ public static class Keys public const string TriggerName = "HX-Trigger-Name"; public const string Trigger = "HX-Trigger"; public const string Boosted = "HX-Boosted"; + + public static string[] All { get; } = new[] + { + CurrentUrl, HistoryRestoreRequest, Prompt, Request, Target, TriggerName, Trigger, Boosted + }; } public HtmxRequestHeaders(HttpRequest request) diff --git a/src/Htmx/HtmxResponseHeaders.cs b/src/Htmx/HtmxResponseHeaders.cs index 1be7cb0..97c16fe 100644 --- a/src/Htmx/HtmxResponseHeaders.cs +++ b/src/Htmx/HtmxResponseHeaders.cs @@ -32,6 +32,11 @@ public static class Keys public const string Reswap = "HX-Reswap"; public const string Retarget = "HX-Retarget"; public const string ReplaceUrl = "HX-Replace-Url"; + + public static string[] All { get; } = new[] + { + PushUrl, Location, Redirect, Refresh, Trigger, TriggerAfterSettle, TriggerAfterSwap, Reswap, Retarget, ReplaceUrl + }; } internal HtmxResponseHeaders(IHeaderDictionary headers) From 305fdb88e6bbe7bdd3c5ff25acfc9c2b6feb1f1a Mon Sep 17 00:00:00 2001 From: Michael Tanczos Date: Tue, 3 Oct 2023 08:44:42 -0400 Subject: [PATCH 3/3] Updated README --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index ca37040..3380ba5 100644 --- a/Readme.md +++ b/Readme.md @@ -82,7 +82,7 @@ Response.Htmx(h => { #### CORS Policy -By default, all Htmx requests and responses will be blocked by the browser in a cross-origin context. +By default, all Htmx requests and responses will be blocked in a cross-origin context. If you configure your application in a cross-origin context, then setting a CORS policy in ASP.NET Core also allows you to define specific restrictions on request and response headers, enabling fine-grained control over the data that can be exchanged between your web application and different origins.