diff --git a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs b/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs deleted file mode 100644 index 27e211f19..000000000 --- a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Server.Transports.AspNetCore; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Net.Http.Headers; - -namespace Squidex.Web.GraphQL; - -public sealed class GraphQLRunner -{ - private readonly GraphQLHttpMiddleware middleware; - - public GraphQLRunner(IServiceProvider serviceProvider) - { - RequestDelegate next = x => Task.CompletedTask; - - var options = new GraphQLHttpMiddlewareOptions - { - DefaultResponseContentType = new MediaTypeHeaderValue("application/json") - }; - - middleware = ActivatorUtilities.CreateInstance>(serviceProvider, next, options); - } - - public Task InvokeAsync(HttpContext context) - { - return middleware.InvokeAsync(context); - } -} diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index 2fa7d726a..b52a7e0d5 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -25,17 +25,21 @@ public sealed class CachingFilter : IAsyncActionFilter public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - var httpContext = context.HttpContext; + if (IgnoreFilter(context)) + { + await next(); + return; + } - cachingManager.Start(httpContext); + cachingManager.Start(context.HttpContext); var resultContext = await next(); - cachingManager.Finish(httpContext); + cachingManager.Finish(context.HttpContext); - if (httpContext.Response.HasStarted == false && - httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && - IsCacheable(httpContext, etag)) + if (context.HttpContext.Response.HasStarted == false && + context.HttpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && + IsCacheable(context.HttpContext, etag)) { resultContext.Result = new StatusCodeResult(304); } @@ -55,4 +59,9 @@ public sealed class CachingFilter : IAsyncActionFilter return ETagUtils.IsSameEtag(noneMatchValue, etag); } + + private static bool IgnoreFilter(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is IgnoreCacheFilterAttribute); + } } diff --git a/backend/src/Squidex.Web/Pipeline/IgnoreCacheFilterAttribute.cs b/backend/src/Squidex.Web/Pipeline/IgnoreCacheFilterAttribute.cs new file mode 100644 index 000000000..3c58b6388 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/IgnoreCacheFilterAttribute.cs @@ -0,0 +1,12 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Web.Pipeline; + +public sealed class IgnoreCacheFilterAttribute : Attribute +{ +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs index db038c060..194f4332b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs @@ -15,6 +15,7 @@ using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; using Squidex.Web.GraphQL; +using Squidex.Web.Pipeline; namespace Squidex.Areas.Api.Controllers.Contents; @@ -22,17 +23,15 @@ namespace Squidex.Areas.Api.Controllers.Contents; public sealed class ContentsSharedController : ApiController { private readonly IContentQueryService contentQuery; - private readonly IContentWorkflow contentWorkflow; private readonly GraphQLRunner graphQLRunner; + private readonly IContentWorkflow contentWorkflow; public ContentsSharedController(ICommandBus commandBus, IContentQueryService contentQuery, - IContentWorkflow contentWorkflow, - GraphQLRunner graphQLRunner) + IContentWorkflow contentWorkflow) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; - this.graphQLRunner = graphQLRunner; } /// @@ -48,9 +47,15 @@ public sealed class ContentsSharedController : ApiController [Route("content/{app}/graphql/batch")] [ApiPermissionOrAnonymous] [ApiCosts(2)] - public Task GetGraphQL(string app) + [IgnoreCacheFilter] + public IActionResult GetGraphQL(string app) { - return graphQLRunner.InvokeAsync(HttpContext); + var options = new GraphQLHttpMiddlewareOptions + { + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; + + return new GraphQLExecutionActionResult(options); } /// diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index a64da5c95..8e376ec03 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -35,9 +35,6 @@ public static class WebServices services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) .AsSelf(); - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs index 616f80d59..d93bad053 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs @@ -61,6 +61,28 @@ public class CachingFilterTests Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); } + [Theory] + [InlineData("13", "13")] + [InlineData("13", "W/13")] + [InlineData("W/13", "13")] + [InlineData("W/13", "W/13")] + public async Task Should_not_return_304_for_same_etags_when_disabled_via_metadata(string ifNoneMatch, string etag) + { + executingContext.ActionDescriptor.EndpointMetadata = new List + { + new IgnoreCacheFilterAttribute() + }; + + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = ifNoneMatch; + + httpContext.Response.Headers[HeaderNames.ETag] = etag; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(200, ((StatusCodeResult)executedContext.Result!).StatusCode); + } + [Fact] public async Task Should_return_304_for_same_etags_from_cache_manager() { diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs index 1850fc86d..333c7ec7d 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -218,18 +218,17 @@ public class AssetFormatTests : IClassFixture { var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?width={width}&height={height}"; - using (var httpClient = _.ClientManager.CreateHttpClient()) - { - var response = await httpClient.GetAsync(url); + var httpClient = _.ClientManager.CreateHttpClient(); + + var response = await httpClient.GetAsync(url); - await using (var stream = await response.Content.ReadAsStreamAsync()) - { - var buffer = new MemoryStream(); + await using (var stream = await response.Content.ReadAsStreamAsync()) + { + var buffer = new MemoryStream(); - await stream.CopyToAsync(buffer); + await stream.CopyToAsync(buffer); - return buffer.Length; - } + return buffer.Length; } } @@ -237,18 +236,17 @@ public class AssetFormatTests : IClassFixture { var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?format={format}"; - using (var httpClient = _.ClientManager.CreateHttpClient()) - { - var response = await httpClient.GetAsync(url); + var httpClient = _.ClientManager.CreateHttpClient(); + + var response = await httpClient.GetAsync(url); - await using (var stream = await response.Content.ReadAsStreamAsync()) - { - var buffer = new MemoryStream(); + await using (var stream = await response.Content.ReadAsStreamAsync()) + { + var buffer = new MemoryStream(); - await stream.CopyToAsync(buffer); + await stream.CopyToAsync(buffer); - return (buffer.Length, response.Content.Headers.ContentType.ToString()); - } + return (buffer.Length, response.Content.Headers.ContentType.ToString()); } } } diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs index 5a8cbf9bb..57113d688 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs @@ -115,16 +115,15 @@ public class ContentLanguageTests : IClassFixture { var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/{_.SchemaName}/{id}"; - using (var httpClient = _.ClientManager.CreateHttpClient()) + var httpClient = _.ClientManager.CreateHttpClient(); + + foreach (var (key, value) in headers) { - foreach (var (key, value) in headers) - { - httpClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); - } + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); + } - var response = await httpClient.GetAsync(url); + var response = await httpClient.GetAsync(url); - return (response.Headers.GetValues("ETag").FirstOrDefault(), response.Headers.Vary.ToString()); - } + return (response.Headers.GetValues("ETag").FirstOrDefault(), response.Headers.Vary.ToString()); } } diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 9f7b63422..f05f2183d 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -698,37 +698,6 @@ public class ContentQueryTests : IClassFixture await _.SharedContents.GraphQlAsync(query); } - [Fact] - public async Task Should_query_correct_content_type_for_graphql() - { - var query = new - { - query = @" - query ContentsQuery($filter: String!) { - queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { - id, - data { - number { - iv - } - } - } - }", - variables = new - { - filter = @"data/number/iv gt 3 and data/number/iv lt 7" - } - }; - - using (var client = _.ClientManager.CreateHttpClient()) - { - // Create the request manually to check the content type. - var response = await client.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); - - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); - } - } - private sealed class QueryResult { [JsonProperty("queryMyReadsContents")] diff --git a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index 5a2dd76cb..299d70ab9 100644 --- a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -7,6 +7,8 @@ using Newtonsoft.Json.Linq; using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Utils; +using System.Net.Http.Json; using TestSuite.Model; #pragma warning disable SA1300 // Element should begin with upper-case letter @@ -224,4 +226,57 @@ public sealed class GraphQLTests : IClassFixture Assert.Equal(new[] { "Sachsen" }, stateNames); } + + [Fact] + public async Task Should_query_correct_content_type_for_graphql() + { + var query = new + { + query = @" + { + queryCitiesContents { + id + } + }" + }; + + var httpClient = _.ClientManager.CreateHttpClient(); + + // Create the request manually to check the content type. + var response = await httpClient.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); + + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public async Task Should_return_correct_vary_headers() + { + var query = new + { + query = @" + { + queryCitiesContents { + id + } + }" + }; + + var httpClient = _.ClientManager.CreateHttpClient(); + + // Create the request manually to check the headers. + var response = await httpClient.PostAsJsonAsync($"api/content/{_.AppName}/graphql", query); + + Assert.Equal(new string[] + { + "Auth-State", + "X-Flatten", + "X-Languages", + "X-NoCleanup", + "X-NoEnrichment", + "X-NoResolveLanguages", + "X-Resolve-Urls", + "X-ResolveFlow", + "X-Unpublished" + }, response.Headers.Vary.Order().ToArray()); + } } diff --git a/tools/TestSuite/docker-compose.yml b/tools/TestSuite/docker-compose.yml index 45ecff5e5..70e865fbb 100644 --- a/tools/TestSuite/docker-compose.yml +++ b/tools/TestSuite/docker-compose.yml @@ -100,7 +100,7 @@ services: - internal squidex_proxy1: - image: squidex/caddy-proxy + image: squidex/caddy-proxy:2.6.2 ports: - "8080:8080" environment: @@ -114,7 +114,7 @@ services: restart: unless-stopped squidex_proxy2: - image: squidex/caddy-proxy-path + image: squidex/caddy-proxy-path:2.6.2 ports: - "8081:8081" environment: @@ -128,7 +128,7 @@ services: restart: unless-stopped squidex_proxy3: - image: squidex/caddy-proxy-path + image: squidex/caddy-proxy-path:2.6.2 ports: - "8082:8082" environment: