Skip to content

Commit

Permalink
Reworked Status Code Handling
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 12, 2022
1 parent e58d4cf commit d1bfcd7
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 64 deletions.
20 changes: 15 additions & 5 deletions src/HotChocolate/AspNetCore/src/AspNetCore/ErrorHelper.cs
Expand Up @@ -28,26 +28,36 @@ public static IError NoSupportedAcceptMediaType()
public static IQueryResult TypeNameIsEmpty()
=> QueryResultBuilder.CreateError(
new Error(
"The specified types argument is empty.",
AspNetCoreResources.ErrorHelper_TypeNameIsEmpty,
code: ErrorCodes.Server.TypeParameterIsEmpty));

public static IQueryResult InvalidTypeName(string typeName)
=> QueryResultBuilder.CreateError(
new Error(
"The type name is invalid.",
AspNetCoreResources.ErrorHelper_InvalidTypeName,
code: ErrorCodes.Server.InvalidTypeName,
extensions: new Dictionary<string, object?>
{
{ "typeName", typeName }
{ nameof(typeName), typeName }
}));

public static IQueryResult TypeNotFound(string typeName)
=> QueryResultBuilder.CreateError(
new Error(
$"The type `{typeName}` does not exist.",
string.Format(AspNetCoreResources.ErrorHelper_TypeNotFound, typeName),
code: ErrorCodes.Server.TypeDoesNotExist,
extensions: new Dictionary<string, object?>
{
{ "typeName", typeName }
{ nameof(typeName), typeName }
}));

public static IQueryResult InvalidAcceptMediaType(string headerValue)
=> QueryResultBuilder.CreateError(
new Error(
string.Format(AspNetCoreResources.ErrorHelper_InvalidAcceptMediaType, headerValue),
code: ErrorCodes.Server.InvalidAcceptHeaderValue,
extensions: new Dictionary<string, object?>
{
{ nameof(headerValue), headerValue }
}));
}
Expand Up @@ -13,7 +13,7 @@ internal static class HttpResponseExtensions
private static readonly JsonSerializerOptions _serializerOptions = new()
{
#if NETCOREAPP3_1
IgnoreNullValues = true,
IgnoreNullValues = true,
#else
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
#endif
Expand Down
59 changes: 51 additions & 8 deletions src/HotChocolate/AspNetCore/src/AspNetCore/HeaderUtilities.cs
@@ -1,7 +1,9 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace HotChocolate.AspNetCore;
Expand All @@ -14,34 +16,45 @@ internal static class HeaderUtilities
private static readonly ConcurrentDictionary<string, CacheEntry> _cache =
new(StringComparer.Ordinal);

public static readonly AcceptMediaType[] GraphQLResponseContentTypes =
{
new AcceptMediaType(
ContentType.Types.Application,
ContentType.SubTypes.GraphQLResponse,
null,
StringSegment.Empty)
};

/// <summary>
/// Gets the parsed accept header values from a request.
/// </summary>
/// <param name="request">
/// The HTTP request.
/// </param>
public static AcceptMediaType[] GetAcceptHeader(HttpRequest request)
public static AcceptHeaderResult GetAcceptHeader(HttpRequest request)
{
if (request.Headers.TryGetValue(HeaderNames.Accept, out var value))
{
var count = value.Count;

if (count == 0)
{
return Array.Empty<AcceptMediaType>();
return new AcceptHeaderResult(Array.Empty<AcceptMediaType>());
}

if (count == 1)
{
if (TryParseMediaType(value[0], out var parsedValue))
var headerValue = value[0]!;

if (TryParseMediaType(headerValue, out var parsedValue))
{
return new[] { parsedValue };
return new AcceptHeaderResult(new[] { parsedValue });
}

return Array.Empty<AcceptMediaType>();
return new AcceptHeaderResult(headerValue);
}

string[] innerArray = value;
string[] innerArray = value!;
ref var searchSpace = ref MemoryMarshal.GetReference(innerArray.AsSpan());
var parsedValues = new AcceptMediaType[innerArray.Length];
var p = 0;
Expand All @@ -53,17 +66,21 @@ public static AcceptMediaType[] GetAcceptHeader(HttpRequest request)
{
parsedValues[p++] = parsedValue;
}
else
{
return new AcceptHeaderResult(mediaType);
}
}

if (parsedValues.Length > p)
{
Array.Resize(ref parsedValues, p);
}

return parsedValues;
return new AcceptHeaderResult(parsedValues);
}

return Array.Empty<AcceptMediaType>();
return new AcceptHeaderResult(Array.Empty<AcceptMediaType>());
}

private static bool TryParseMediaType(string s, out AcceptMediaType value)
Expand Down Expand Up @@ -118,4 +135,30 @@ public CacheEntry(string key, MediaTypeHeaderValue value)

public DateTime CreatedAt { get; }
}

internal readonly struct AcceptHeaderResult
{
public AcceptHeaderResult(AcceptMediaType[] acceptMediaTypes)
{
AcceptMediaTypes = acceptMediaTypes;
ErrorResult = null;
HasError = false;
}

public AcceptHeaderResult(string headerValue)
{
AcceptMediaTypes = Array.Empty<AcceptMediaType>();
ErrorResult = ErrorHelper.InvalidAcceptMediaType(headerValue);
HasError = true;
}

public AcceptMediaType[] AcceptMediaTypes { get; }

public IQueryResult? ErrorResult { get; }

#if NET5_0_OR_GREATER
[MemberNotNullWhen(true, nameof(ErrorResult))]
#endif
public bool HasError { get; }
}
}
33 changes: 29 additions & 4 deletions src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetMiddleware.cs
Expand Up @@ -64,14 +64,39 @@ private async Task HandleRequestAsync(HttpContext context)
context.Items[WellKnownContextData.RequestExecutor] = requestExecutor;

// next we will inspect the accept headers and determine if we can execute this request.
// if the request defines the accept header value and we cannot meet any of the provided
// media types we will fail the request with 406 Not Acceptable.
var acceptMediaTypes = HeaderUtilities.GetAcceptHeader(context.Request);
var requestFlags = CreateRequestFlags(acceptMediaTypes);
var headerResult = HeaderUtilities.GetAcceptHeader(context.Request);
var acceptMediaTypes = headerResult.AcceptMediaTypes;

// if we cannot parse all media types that we provided we will fail the request
// with a 400 Bad Request.
if (headerResult.HasError)
{
// in this case accept headers were specified and we will
// respond with proper error codes
acceptMediaTypes = HeaderUtilities.GraphQLResponseContentTypes;
statusCode = HttpStatusCode.BadRequest;

#if NET5_0_OR_GREATER
var errors = headerResult.ErrorResult.Errors!;
#else
var errors = headerResult.ErrorResult!.Errors!;
#endif
result = headerResult.ErrorResult;
_diagnosticEvents.HttpRequestError(context, errors[0]);
goto HANDLE_RESULT;
}

var requestFlags = CreateRequestFlags(headerResult.AcceptMediaTypes);

// if the request defines accept header values of which we cannot handle any provided
// media type then we will fail the request with 406 Not Acceptable.
if (requestFlags is None)
{
// in this case accept headers were specified and we will
// respond with proper error codes
acceptMediaTypes = HeaderUtilities.GraphQLResponseContentTypes;
statusCode = HttpStatusCode.NotAcceptable;

var error = ErrorHelper.NoSupportedAcceptMediaType();
result = QueryResultBuilder.CreateError(error);
_diagnosticEvents.HttpRequestError(context, error);
Expand Down
117 changes: 71 additions & 46 deletions src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddlewareBase.cs
Expand Up @@ -2,10 +2,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Http;
using HotChocolate.AspNetCore.Instrumentation;
using HotChocolate.AspNetCore.Serialization;
using HotChocolate.Language;
using Microsoft.AspNetCore.Http;
using static HotChocolate.Execution.GraphQLRequestFlags;
using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate;

Expand Down Expand Up @@ -69,14 +69,39 @@ protected async Task HandleRequestAsync(HttpContext context)
context.Items[WellKnownContextData.RequestExecutor] = requestExecutor;

// next we will inspect the accept headers and determine if we can execute this request.
// if the request defines the accept header value and we cannot meet any of the provided
// media types we will fail the request with 406 Not Acceptable.
var acceptMediaTypes = HeaderUtilities.GetAcceptHeader(context.Request);
var requestFlags = CreateRequestFlags(acceptMediaTypes);
var headerResult = HeaderUtilities.GetAcceptHeader(context.Request);
var acceptMediaTypes = headerResult.AcceptMediaTypes;

// if we cannot parse all media types that we provided we will fail the request
// with a 400 Bad Request.
if (headerResult.HasError)
{
// in this case accept headers were specified and we will
// respond with proper error codes
acceptMediaTypes = HeaderUtilities.GraphQLResponseContentTypes;
statusCode = HttpStatusCode.BadRequest;

#if NET5_0_OR_GREATER
var errors = headerResult.ErrorResult.Errors!;
#else
var errors = headerResult.ErrorResult!.Errors!;
#endif
result = headerResult.ErrorResult;
DiagnosticEvents.HttpRequestError(context, errors[0]);
goto HANDLE_RESULT;
}

var requestFlags = CreateRequestFlags(headerResult.AcceptMediaTypes);

// if the request defines accept header values of which we cannot handle any provided
// media type then we will fail the request with 406 Not Acceptable.
if (requestFlags is None)
{
// in this case accept headers were specified and we will
// respond with proper error codes
acceptMediaTypes = HeaderUtilities.GraphQLResponseContentTypes;
statusCode = HttpStatusCode.NotAcceptable;

var error = ErrorHelper.NoSupportedAcceptMediaType();
result = QueryResultBuilder.CreateError(error);
DiagnosticEvents.HttpRequestError(context, error);
Expand Down Expand Up @@ -121,13 +146,13 @@ protected async Task HandleRequestAsync(HttpContext context)
// if the HTTP request body contains no GraphQL request structure the
// whole request is invalid and we will create a GraphQL error response.
case 0:
{
statusCode = HttpStatusCode.BadRequest;
var error = errorHandler.Handle(ErrorHelper.RequestHasNoElements());
result = QueryResultBuilder.CreateError(error);
DiagnosticEvents.HttpRequestError(context, error);
break;
}
{
statusCode = HttpStatusCode.BadRequest;
var error = errorHandler.Handle(ErrorHelper.RequestHasNoElements());
result = QueryResultBuilder.CreateError(error);
DiagnosticEvents.HttpRequestError(context, error);
break;
}

// if the HTTP request body contains a single GraphQL request and we do have
// the batch operations query parameter specified we need to execute an
Expand All @@ -137,49 +162,49 @@ protected async Task HandleRequestAsync(HttpContext context)
// contains multiple operations. The batch operation query parameter
// defines the order in which the operations shall be executed.
case 1 when context.Request.Query.ContainsKey(_batchOperations):
{
string? operationNames = context.Request.Query[_batchOperations];

if (!string.IsNullOrEmpty(operationNames) &&
TryParseOperations(operationNames, out var ops))
{
result = await ExecuteOperationBatchAsync(
context,
requestExecutor,
requestInterceptor,
DiagnosticEvents,
requests[0],
requestFlags,
ops);
}
else
{
var error = errorHandler.Handle(ErrorHelper.InvalidRequest());
statusCode = HttpStatusCode.BadRequest;
result = QueryResultBuilder.CreateError(error);
DiagnosticEvents.HttpRequestError(context, error);
string? operationNames = context.Request.Query[_batchOperations];

if (!string.IsNullOrEmpty(operationNames) &&
TryParseOperations(operationNames, out var ops))
{
result = await ExecuteOperationBatchAsync(
context,
requestExecutor,
requestInterceptor,
DiagnosticEvents,
requests[0],
requestFlags,
ops);
}
else
{
var error = errorHandler.Handle(ErrorHelper.InvalidRequest());
statusCode = HttpStatusCode.BadRequest;
result = QueryResultBuilder.CreateError(error);
DiagnosticEvents.HttpRequestError(context, error);
}

break;
}

break;
}

// if the HTTP request body contains a single GraphQL request and
// no batch query parameter is specified we need to execute a single
// GraphQL request.
//
// Most GraphQL requests will be of this type where we want to execute
// a single GraphQL query or mutation.
case 1:
{
result = await ExecuteSingleAsync(
context,
requestExecutor,
requestInterceptor,
DiagnosticEvents,
requests[0],
requestFlags);
break;
}
{
result = await ExecuteSingleAsync(
context,
requestExecutor,
requestInterceptor,
DiagnosticEvents,
requests[0],
requestFlags);
break;
}

// if the HTTP request body contains more than one GraphQL request than
// we need to execute a request batch where we need to execute multiple
Expand Down Expand Up @@ -214,7 +239,7 @@ protected async Task HandleRequestAsync(HttpContext context)
DiagnosticEvents.HttpRequestError(context, error);
}

HANDLE_RESULT:
HANDLE_RESULT:
IDisposable? formatScope = null;

try
Expand Down

0 comments on commit d1bfcd7

Please sign in to comment.