Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;
using HotChocolate.AspNetCore.Formatters;
using HotChocolate.AspNetCore.Tests.Utilities;
using HotChocolate.Features;
using HotChocolate.Transport;
using HotChocolate.Transport.Http;
using HotChocolate.Types;
Expand All @@ -15,6 +16,7 @@
using static System.Net.HttpStatusCode;
using static HotChocolate.AspNetCore.HttpTransportVersion;
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
using OperationInfo = HotChocolate.Execution.Pipeline.OperationInfo;
using WellKnownRequestMiddleware = HotChocolate.Execution.WellKnownRequestMiddleware;

namespace HotChocolate.AspNetCore;
Expand Down Expand Up @@ -982,6 +984,287 @@ public async Task Post_Should_ReturnInternalServerError_When_PipelineThrowsUnexp
Assert.Equal(expectedContentType, response.Content.Headers.ContentType?.ToString());
}

// A pipeline step that finds the request state an earlier step provides missing is a failure
// inside the server, and is answered 500 like one.
[Theory]
[InlineData("""{ "query": "{ __typename }" }""", Draft20250508)]
[InlineData("""{ "query": "{ __typename }" }""", Draft20260903)]
[InlineData("""{ "id": "60ddx_GGk4FDObSa6eK0sg" }""", Draft20250508)]
[InlineData("""{ "id": "60ddx_GGk4FDObSa6eK0sg" }""", Draft20260903)]
Comment thread
glen-84 marked this conversation as resolved.
public async Task Post_Should_ReturnInternalServerError_When_DocumentIsMissingBeforeValidation(
string body,
HttpTransportVersion transportVersion)
{
// arrange
var client = GetClient(
transportVersion,
WellKnownRequestMiddleware.DocumentValidationMiddleware,
context => context.OperationDocumentInfo.Document = null);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
-------------------------->
{"errors":[{"message":"The query request contains no document or no document id.","extensions":{"code":"HC0015"}}]}
""");
}

[Theory]
[InlineData(Draft20250508)]
[InlineData(Draft20260903)]
public async Task Post_Should_ReturnInternalServerError_When_CachedDocumentIsMissing(
HttpTransportVersion transportVersion)
{
// arrange
// the first request puts its document into the document cache under the document ID
var client = GetClient(
transportVersion,
WellKnownRequestMiddleware.DocumentValidationMiddleware,
context =>
{
if (context.OperationDocumentInfo.IsCached)
{
context.OperationDocumentInfo.Document = null;
}
});

using var cacheRequest = new HttpRequestMessage(HttpMethod.Post, s_url);
cacheRequest.Content = new StringContent(
"""{ "id": "cached-document", "query": "{ __typename }" }""",
Encoding.UTF8,
"application/json");
using var cacheResponse = await client.SendAsync(
cacheRequest,
TestContext.Current.CancellationToken);
Assert.Equal(OK, cacheResponse.StatusCode);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = new StringContent(
"""{ "id": "cached-document" }""",
Encoding.UTF8,
"application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
-------------------------->
{"errors":[{"message":"The query request contains no document or no document id.","extensions":{"code":"HC0015"}}]}
""");
}

// A request that carries only a document ID, sent to a pipeline without persisted
// operations, is a request error.
[Theory]
[InlineData(Draft20250508, BadRequest)]
[InlineData(Draft20260903, UnprocessableContent)]
public async Task Post_Should_ReturnUnprocessableContent_When_DocumentIdIsNotResolved(
HttpTransportVersion transportVersion,
HttpStatusCode expectedStatusCode)
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQLServer("test")
.AddQueryType(d => d.Name("Query").Field("foo").Resolve("bar"))
.AddHttpResponseFormatter(
new HttpResponseFormatterOptions
{
HttpTransportVersion = transportVersion
}));
var client = server.CreateClient();

// act
using var request = new HttpRequestMessage(
HttpMethod.Post,
new Uri("http://localhost:5000/test"));
request.Content = new StringContent("""{ "id": "abc" }""", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
$$$"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
-------------------------->
{"errors":[{"message":"The query request contains no document or no document id.","extensions":{"code":"HC0015"}}]}
""");
}

[Theory]
[InlineData(Draft20250508)]
[InlineData(Draft20260903)]
public async Task Post_Should_ReturnInternalServerError_When_DocumentIsMissingBeforeCompilation(
HttpTransportVersion transportVersion)
{
// arrange
var client = GetClient(
transportVersion,
WellKnownRequestMiddleware.OperationCompilerMiddleware,
context => context.OperationDocumentInfo.Document = null);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" });

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
-------------------------->
{"errors":[{"message":"Either no query document exists or the document validation result is invalid."}]}
""");
}

[Theory]
[InlineData(Draft20250508)]
[InlineData(Draft20260903)]
public async Task Post_Should_ReturnInternalServerError_When_OperationIsMissingBeforeExecution(
HttpTransportVersion transportVersion)
{
// arrange
var client = GetClient(
transportVersion,
WellKnownRequestMiddleware.OperationExecutionMiddleware,
context => context.Features.GetRequired<OperationInfo>().Operation = null);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" });

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
-------------------------->
{"errors":[{"message":"Either no compiled operation was found or the variables have not been coerced."}]}
""");
}

[Theory]
[InlineData("""{ "query": "{ __typename }" }""", Draft20250508)]
[InlineData("""{ "query": "{ __typename }" }""", Draft20260903)]
[InlineData("""{ "query": "{ __typename }", "variables": [{}] }""", Draft20250508)]
[InlineData("""{ "query": "{ __typename }", "variables": [{}] }""", Draft20260903)]
public async Task Post_Should_ReturnInternalServerError_When_VariablesAreMissingBeforeExecution(
string body,
HttpTransportVersion transportVersion)
{
// arrange
var client = GetClient(
transportVersion,
WellKnownRequestMiddleware.OperationExecutionMiddleware,
context => context.VariableValues = []);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
-------------------------->
{"errors":[{"message":"Either no compiled operation was found or the variables have not been coerced."}]}
""");
}

[Theory]
[InlineData(Draft20250508, BadRequest)]
[InlineData(Draft20260903, UnprocessableContent)]
public async Task Post_Should_ReturnUnprocessableContent_When_VariableBatchIsEmpty(
HttpTransportVersion transportVersion,
HttpStatusCode expectedStatusCode)
{
// arrange
// with the cost analyzer skipped, the empty batch reaches operation execution
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQLServer()
.ModifyCostOptions(o => o.SkipAnalyzer = true)
.AddHttpResponseFormatter(
new HttpResponseFormatterOptions
{
HttpTransportVersion = transportVersion
}));
var client = server.CreateClient();

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = new StringContent(
"""{ "query": "query($id: String!) { human(id: $id) { name } }", "variables": [] }""",
Encoding.UTF8,
"application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
$$$"""
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
-------------------------->
{"errors":[{"message":"A variable batch request must contain at least one variable set."}]}
""");
}

[Theory]
[InlineData("application/json")]
[InlineData("Application/Json")]
Expand Down Expand Up @@ -2024,6 +2307,31 @@ private HttpClient GetClient(HttpTransportVersion serverTransportVersion)
return server.CreateClient();
}

private HttpClient GetClient(
HttpTransportVersion serverTransportVersion,
string nextMiddleware,
Action<Execution.RequestContext> modifyContext)
{
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQLServer()
.UseRequest(
next => context =>
{
modifyContext(context);
return next(context);
},
key: "ModifyContext",
before: nextMiddleware)
.AddHttpResponseFormatter(
new HttpResponseFormatterOptions
{
HttpTransportVersion = serverTransportVersion
}));

return server.CreateClient();
}

private HttpClient GetQueryClient(HttpTransportVersion serverTransportVersion)
{
var server = CreateStarWarsServer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ private DocumentValidationMiddleware(
public async ValueTask InvokeAsync(RequestContext context)
{
var documentInfo = context.OperationDocumentInfo;
if (documentInfo.Document is null || documentInfo.Id.IsEmpty)
if (documentInfo.Document is null
&& context.Request.Document is null
&& !documentInfo.IsCached
&& !documentInfo.IsPersisted)
{
// The request carries only a document ID or hash that no earlier step resolved.
context.Result = ErrorHelper.OperationDocumentNotFound();
}
else if (documentInfo.Document is null || documentInfo.Id.IsEmpty)
{
context.Result = ErrorHelper.StateInvalidForDocumentValidation();
}
Expand Down
14 changes: 13 additions & 1 deletion src/HotChocolate/Core/src/Execution.Pipeline/ErrorHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Net;
using HotChocolate.Execution.Pipeline.Properties;

namespace HotChocolate.Execution.Pipeline;
Expand All @@ -16,10 +17,21 @@ public static IError OperationCanceled(Exception ex)
public static NotSupportedException QueryTypeNotSupported()
=> new(ExecutionPipelineResources.ThrowHelper_QueryTypeNotSupported_Message);

public static OperationResult StateInvalidForDocumentValidation()
public static OperationResult OperationDocumentNotFound()
=> OperationResult.FromError(
ErrorBuilder.New()
.SetMessage(ExecutionPipelineResources.ErrorHelper_StateInvalidForDocumentValidation_Message)
.SetCode(ErrorCodes.Execution.OperationDocumentNotFound)
.Build());

public static OperationResult StateInvalidForDocumentValidation()
{
var result = OperationDocumentNotFound();

result.ContextData = result.ContextData.Add(
ExecutionContextData.HttpStatusCode,
HttpStatusCode.InternalServerError);

return result;
}
}
Loading
Loading