diff --git a/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs b/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs index 43a834a2..451c16cb 100644 --- a/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs +++ b/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs @@ -141,6 +141,7 @@ protected virtual void HandleNodeNotAuthorized(ValidationInfo info) { var resource = GenerateResourceDescription(info); var err = info.Node == null ? new AccessDeniedError(resource) : new AccessDeniedError(resource, info.Context.Document.Source, info.Node); + err.PreferredStatusCode = HttpStatusCode.Unauthorized; info.Context.ReportError(err); } diff --git a/src/Transports.AspNetCore/Errors/AccessDeniedError.cs b/src/Transports.AspNetCore/Errors/AccessDeniedError.cs index 7ad2c685..b678a8ea 100644 --- a/src/Transports.AspNetCore/Errors/AccessDeniedError.cs +++ b/src/Transports.AspNetCore/Errors/AccessDeniedError.cs @@ -3,7 +3,7 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors; /// /// Represents an error indicating that the user is not allowed access to the specified resource. /// -public class AccessDeniedError : ValidationError +public class AccessDeniedError : ValidationError, IHasPreferredStatusCode { /// public AccessDeniedError(string resource) @@ -29,4 +29,7 @@ public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, param /// Returns the list of role memberships that would allow access to these node(s). /// public List? RolesRequired { get; set; } + + /// + public HttpStatusCode PreferredStatusCode { get; set; } = HttpStatusCode.Forbidden; } diff --git a/src/Transports.AspNetCore/Errors/FileCountExceededError.cs b/src/Transports.AspNetCore/Errors/FileCountExceededError.cs index 0d5733cc..79f6a4b9 100644 --- a/src/Transports.AspNetCore/Errors/FileCountExceededError.cs +++ b/src/Transports.AspNetCore/Errors/FileCountExceededError.cs @@ -14,5 +14,5 @@ public FileCountExceededError() } /// - public HttpStatusCode PreferredStatusCode => HttpStatusCode.RequestEntityTooLarge; + public HttpStatusCode PreferredStatusCode { get; set; } = HttpStatusCode.RequestEntityTooLarge; } diff --git a/src/Transports.AspNetCore/Errors/FileSizeExceededError.cs b/src/Transports.AspNetCore/Errors/FileSizeExceededError.cs index 67b998c5..9f897d53 100644 --- a/src/Transports.AspNetCore/Errors/FileSizeExceededError.cs +++ b/src/Transports.AspNetCore/Errors/FileSizeExceededError.cs @@ -14,5 +14,5 @@ public FileSizeExceededError() } /// - public HttpStatusCode PreferredStatusCode => HttpStatusCode.RequestEntityTooLarge; + public HttpStatusCode PreferredStatusCode { get; set; } = HttpStatusCode.RequestEntityTooLarge; } diff --git a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs index 83194aa2..8fcc9872 100644 --- a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs +++ b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs @@ -3,11 +3,14 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors; /// /// Represents an error indicating that the content-type is invalid, for example, could not be parsed or is not supported. /// -public class InvalidContentTypeError : RequestError +public class InvalidContentTypeError : RequestError, IHasPreferredStatusCode { /// public InvalidContentTypeError() : base("Invalid 'Content-Type' header.") { } /// public InvalidContentTypeError(string message) : base("Invalid 'Content-Type' header: " + message) { } + + /// + public HttpStatusCode PreferredStatusCode { get; set; } = HttpStatusCode.UnsupportedMediaType; } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index a8ed6d37..5d493710 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -273,7 +273,7 @@ protected virtual async Task InvokeAsync(HttpContext context, RequestDelegate ne } catch (ExecutionError ex) // catches FileCountExceededError, FileSizeExceededError, InvalidMapError { - await WriteErrorResponseAsync(context, ex is IHasPreferredStatusCode sc ? sc.PreferredStatusCode : HttpStatusCode.BadRequest, ex); + await WriteErrorResponseAsync(context, ex); return null; } catch (Exception ex) // catches JSON deserialization exceptions @@ -1046,19 +1046,19 @@ await webSocket.CloseAsync( /// Writes an access denied message to the output with status code 401 Unauthorized when the user is not authenticated. /// protected virtual Task HandleNotAuthenticatedAsync(HttpContext context, RequestDelegate next) - => WriteErrorResponseAsync(context, HttpStatusCode.Unauthorized, new AccessDeniedError("schema")); + => WriteErrorResponseAsync(context, new AccessDeniedError("schema") { PreferredStatusCode = HttpStatusCode.Unauthorized }); /// /// Writes an access denied message to the output with status code 403 Forbidden when the user fails the role checks. /// protected virtual Task HandleNotAuthorizedRoleAsync(HttpContext context, RequestDelegate next) - => WriteErrorResponseAsync(context, HttpStatusCode.Forbidden, new AccessDeniedError("schema") { RolesRequired = _options.AuthorizedRoles }); + => WriteErrorResponseAsync(context, new AccessDeniedError("schema") { RolesRequired = _options.AuthorizedRoles }); /// /// Writes an access denied message to the output with status code 403 Forbidden when the user fails the policy check. /// protected virtual Task HandleNotAuthorizedPolicyAsync(HttpContext context, RequestDelegate next, AuthorizationResult authorizationResult) - => WriteErrorResponseAsync(context, HttpStatusCode.Forbidden, new AccessDeniedError("schema") { PolicyRequired = _options.AuthorizedPolicy, PolicyAuthorizationResult = authorizationResult }); + => WriteErrorResponseAsync(context, new AccessDeniedError("schema") { PolicyRequired = _options.AuthorizedPolicy, PolicyAuthorizationResult = authorizationResult }); /// /// Writes a '400 JSON body text could not be parsed.' message to the output. @@ -1095,7 +1095,7 @@ protected virtual Task HandleWebSocketSubProtocolNotSupportedAsync(HttpContext c /// Writes a '415 Invalid Content-Type header: could not be parsed.' message to the output. /// protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context, RequestDelegate next) - => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"value '{context.Request.ContentType}' could not be parsed.")); + => WriteErrorResponseAsync(context, new InvalidContentTypeError($"value '{context.Request.ContentType}' could not be parsed.")); /// /// Writes a '415 Invalid Content-Type header: non-supported media type.' message to the output. @@ -1103,7 +1103,6 @@ protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext c protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context, RequestDelegate next) => WriteErrorResponseAsync( context, - HttpStatusCode.UnsupportedMediaType, _options.ReadFormOnPost ? new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}', '{MEDIATYPE_GRAPHQL}' or a form body.") : new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}' or '{MEDIATYPE_GRAPHQL}'.") diff --git a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt index d72fac9e..279e2717 100644 --- a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -192,12 +192,13 @@ namespace GraphQL.Server.Transports.AspNetCore } namespace GraphQL.Server.Transports.AspNetCore.Errors { - public class AccessDeniedError : GraphQL.Validation.ValidationError + public class AccessDeniedError : GraphQL.Validation.ValidationError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public AccessDeniedError(string resource) { } public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params GraphQLParser.AST.ASTNode[] nodes) { } public Microsoft.AspNetCore.Authorization.AuthorizationResult? PolicyAuthorizationResult { get; set; } public string? PolicyRequired { get; set; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } public System.Collections.Generic.List? RolesRequired { get; set; } } public class BatchedRequestsNotSupportedError : GraphQL.Execution.RequestError @@ -212,12 +213,12 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors public class FileCountExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileCountExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class FileSizeExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileSizeExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class HttpMethodValidationError : GraphQL.Validation.ValidationError { @@ -227,10 +228,11 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors { System.Net.HttpStatusCode PreferredStatusCode { get; } } - public class InvalidContentTypeError : GraphQL.Execution.RequestError + public class InvalidContentTypeError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public InvalidContentTypeError() { } public InvalidContentTypeError(string message) { } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class InvalidMapError : GraphQL.Execution.RequestError { diff --git a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt index 3de02297..e1a2d4c7 100644 --- a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -210,12 +210,13 @@ namespace GraphQL.Server.Transports.AspNetCore } namespace GraphQL.Server.Transports.AspNetCore.Errors { - public class AccessDeniedError : GraphQL.Validation.ValidationError + public class AccessDeniedError : GraphQL.Validation.ValidationError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public AccessDeniedError(string resource) { } public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params GraphQLParser.AST.ASTNode[] nodes) { } public Microsoft.AspNetCore.Authorization.AuthorizationResult? PolicyAuthorizationResult { get; set; } public string? PolicyRequired { get; set; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } public System.Collections.Generic.List? RolesRequired { get; set; } } public class BatchedRequestsNotSupportedError : GraphQL.Execution.RequestError @@ -230,12 +231,12 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors public class FileCountExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileCountExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class FileSizeExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileSizeExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class HttpMethodValidationError : GraphQL.Validation.ValidationError { @@ -245,10 +246,11 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors { System.Net.HttpStatusCode PreferredStatusCode { get; } } - public class InvalidContentTypeError : GraphQL.Execution.RequestError + public class InvalidContentTypeError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public InvalidContentTypeError() { } public InvalidContentTypeError(string message) { } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class InvalidMapError : GraphQL.Execution.RequestError { diff --git a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt index 2122e599..6ed0eab4 100644 --- a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -192,12 +192,13 @@ namespace GraphQL.Server.Transports.AspNetCore } namespace GraphQL.Server.Transports.AspNetCore.Errors { - public class AccessDeniedError : GraphQL.Validation.ValidationError + public class AccessDeniedError : GraphQL.Validation.ValidationError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public AccessDeniedError(string resource) { } public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params GraphQLParser.AST.ASTNode[] nodes) { } public Microsoft.AspNetCore.Authorization.AuthorizationResult? PolicyAuthorizationResult { get; set; } public string? PolicyRequired { get; set; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } public System.Collections.Generic.List? RolesRequired { get; set; } } public class BatchedRequestsNotSupportedError : GraphQL.Execution.RequestError @@ -212,12 +213,12 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors public class FileCountExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileCountExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class FileSizeExceededError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public FileSizeExceededError() { } - public System.Net.HttpStatusCode PreferredStatusCode { get; } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class HttpMethodValidationError : GraphQL.Validation.ValidationError { @@ -227,10 +228,11 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors { System.Net.HttpStatusCode PreferredStatusCode { get; } } - public class InvalidContentTypeError : GraphQL.Execution.RequestError + public class InvalidContentTypeError : GraphQL.Execution.RequestError, GraphQL.Server.Transports.AspNetCore.Errors.IHasPreferredStatusCode { public InvalidContentTypeError() { } public InvalidContentTypeError(string message) { } + public System.Net.HttpStatusCode PreferredStatusCode { get; set; } } public class InvalidMapError : GraphQL.Execution.RequestError {