From 4deb756e644e9ac31fb38fc337356db62452cbef Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 11 Jul 2025 15:51:05 +0200 Subject: [PATCH] Adds variation by the header name Accept-Language to the delivery API output cache policy (#19709) * Adds variation by the header name Accept-Language to the develivery API output cache policy * Removed obsolete constructor (not necessary as the class is internal). * Introduce contants for header names. --- .../Caching/DeliveryApiOutputCachePolicy.cs | 12 ++++-- .../UmbracoBuilderExtensions.cs | 13 ++++++- .../SwaggerContentDocumentationFilter.cs | 10 ++--- .../Filters/SwaggerDocumentationFilterBase.cs | 4 +- .../Services/ApiAccessService.cs | 4 +- .../Services/RequestPreviewService.cs | 2 +- .../Services/RequestSegmentService.cs | 4 +- .../Services/RequestStartItemProvider.cs | 2 +- src/Umbraco.Core/Constants-DeliveryApi.cs | 37 +++++++++++++++++-- 9 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs index da1580554c..2c626d0bfb 100644 --- a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs +++ b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs @@ -1,5 +1,6 @@ -using Microsoft.AspNetCore.OutputCaching; +using Microsoft.AspNetCore.OutputCaching; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core.DeliveryApi; namespace Umbraco.Cms.Api.Delivery.Caching; @@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching; internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy { private readonly TimeSpan _duration; + private readonly StringValues _varyByHeaderNames; - public DeliveryApiOutputCachePolicy(TimeSpan duration) - => _duration = duration; + public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames) + { + _duration = duration; + _varyByHeaderNames = varyByHeaderNames; + } ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { @@ -20,6 +25,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy context.EnableOutputCaching = requestPreviewService.IsPreview() is false; context.ResponseExpirationTimeSpan = _duration; + context.CacheVaryByRules.HeaderNames = _varyByHeaderNames; return ValueTask.CompletedTask; } diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index ca64e175a0..6557cd17dc 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using Umbraco.Cms.Api.Common.DependencyInjection; using Umbraco.Cms.Api.Delivery.Accessors; using Umbraco.Cms.Api.Delivery.Caching; @@ -110,12 +111,20 @@ public static class UmbracoBuilderExtensions if (outputCacheSettings.ContentDuration.TotalSeconds > 0) { - options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration)); + options.AddPolicy( + Constants.DeliveryApi.OutputCache.ContentCachePolicy, + new DeliveryApiOutputCachePolicy( + outputCacheSettings.ContentDuration, + new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem]))); } if (outputCacheSettings.MediaDuration.TotalSeconds > 0) { - options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration)); + options.AddPolicy( + Constants.DeliveryApi.OutputCache.MediaCachePolicy, + new DeliveryApiOutputCachePolicy( + outputCacheSettings.MediaDuration, + Constants.DeliveryApi.HeaderNames.StartItem)); } }); diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs index 39d7125e55..014aed28c8 100644 --- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs @@ -1,4 +1,4 @@ -using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Delivery.Configuration; @@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi operation.Parameters.Add(new OpenApiParameter { - Name = "Accept-Language", + Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage, In = ParameterLocation.Header, Required = false, Description = "Defines the language to return. Use this when querying language variant content items.", @@ -35,7 +35,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi operation.Parameters.Add(new OpenApiParameter { - Name = "Accept-Segment", + Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment, In = ParameterLocation.Header, Required = false, Description = "Defines the segment to return. Use this when querying segment variant content items.", @@ -51,7 +51,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi operation.Parameters.Add(new OpenApiParameter { - Name = "Preview", + Name = Core.Constants.DeliveryApi.HeaderNames.Preview, In = ParameterLocation.Header, Required = false, Description = "Whether to request draft content.", @@ -60,7 +60,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi operation.Parameters.Add(new OpenApiParameter { - Name = "Start-Item", + Name = Core.Constants.DeliveryApi.HeaderNames.StartItem, In = ParameterLocation.Header, Required = false, Description = "URL segment or GUID of a root content item.", diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs index 52acddaca9..8a450a2f9b 100644 --- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase protected void AddApiKey(OpenApiOperation operation) => operation.Parameters.Add(new OpenApiParameter { - Name = "Api-Key", + Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey, In = ParameterLocation.Header, Required = false, Description = "API key specified through configuration to authorize access to the API.", diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs index 0ba7df9b49..0a581af9f5 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; @@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService private bool IfEnabled(Func condition) => _deliveryApiSettings.Enabled && condition(); private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false - && _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key")); + && _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey)); private bool IfMediaEnabled(Func condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition(); } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs index f891aee689..25689125f5 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs @@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev } /// - public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase); + public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase); } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestSegmentService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestSegmentService.cs index 46a689c539..c59c2fb56e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestSegmentService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestSegmentService.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.DeliveryApi; namespace Umbraco.Cms.Api.Delivery.Services; @@ -12,5 +12,5 @@ internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegm /// public string? GetRequestedSegment() - => GetHeaderValue("Accept-Segment"); + => GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.AcceptSegment); } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs index 92aff37011..87e27b535b 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs @@ -60,5 +60,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS } /// - public string? RequestedStartItem() => GetHeaderValue("Start-Item"); + public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem); } diff --git a/src/Umbraco.Core/Constants-DeliveryApi.cs b/src/Umbraco.Core/Constants-DeliveryApi.cs index 85677a23bc..8d66a53de9 100644 --- a/src/Umbraco.Core/Constants-DeliveryApi.cs +++ b/src/Umbraco.Core/Constants-DeliveryApi.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core; +namespace Umbraco.Cms.Core; public static partial class Constants { @@ -24,14 +24,45 @@ public static partial class Constants public static class OutputCache { /// - /// Output cache policy name for content + /// Output cache policy name for content. /// public const string ContentCachePolicy = "DeliveryApiContent"; /// - /// Output cache policy name for media + /// Output cache policy name for media. /// public const string MediaCachePolicy = "DeliveryApiMedia"; } + + /// + /// Constants for Delivery API header names. + /// + public static class HeaderNames + { + /// + /// Header name for accept language. + /// + public const string AcceptLanguage = "Accept-Language"; + + /// + /// Header name for accept segment. + /// + public const string AcceptSegment = "Accept-Segment"; + + /// + /// Header name for API key. + /// + public const string ApiKey = "Api-Key"; + + /// + /// Header name for preview. + /// + public const string Preview = "Preview"; + + /// + /// Header name for start item. + /// + public const string StartItem = "Start-Item"; + } } }