Browse Source

Graphql cache fix (#968)

* Use old method again.

* Proper graphql header fix.

* Use newer caddy version.

* Fix tests
pull/971/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
6351f8e865
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs
  2. 21
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  3. 12
      backend/src/Squidex.Web/Pipeline/IgnoreCacheFilterAttribute.cs
  4. 17
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  5. 3
      backend/src/Squidex/Config/Web/WebServices.cs
  6. 22
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  7. 34
      tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
  8. 15
      tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs
  9. 31
      tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  10. 55
      tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  11. 6
      tools/TestSuite/docker-compose.yml

35
backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs

@ -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<DummySchema> middleware;
public GraphQLRunner(IServiceProvider serviceProvider)
{
RequestDelegate next = x => Task.CompletedTask;
var options = new GraphQLHttpMiddlewareOptions
{
DefaultResponseContentType = new MediaTypeHeaderValue("application/json")
};
middleware = ActivatorUtilities.CreateInstance<GraphQLHttpMiddleware<DummySchema>>(serviceProvider, next, options);
}
public Task InvokeAsync(HttpContext context)
{
return middleware.InvokeAsync(context);
}
}

21
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);
}
}

12
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
{
}

17
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;
}
/// <summary>
@ -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<DummySchema>(options);
}
/// <summary>

3
backend/src/Squidex/Config/Web/WebServices.cs

@ -35,9 +35,6 @@ public static class WebServices
services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly))
.AsSelf();
services.AddSingletonAs<GraphQLRunner>()
.AsSelf();
services.AddSingletonAs<FileCallbackResultExecutor>()
.AsSelf();

22
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<object>
{
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()
{

34
tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs

@ -218,18 +218,17 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
{
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<CreatedAppFixture>
{
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());
}
}
}

15
tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs

@ -115,16 +115,15 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
{
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());
}
}

31
tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -698,37 +698,6 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
await _.SharedContents.GraphQlAsync<QueryResult>(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")]

55
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<GraphQLFixture>
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());
}
}

6
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:

Loading…
Cancel
Save