From e4b942d06223dd8566624939114cc7dbfa8a01ef Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Sun, 22 Dec 2024 05:25:19 +0300 Subject: [PATCH] refactor: to return the result according to the RFC 7807 standard and add a traceId --- ApiDto/Responses/ErrorResponse.cs | 22 ------ .../Attributes/BadRequestResponseAttribute.cs | 3 +- .../Attributes/NotFoundResponseAttribute.cs | 3 +- .../CustomExceptionHandlerMiddleware.cs | 68 +++++++++++-------- .../Core/Startup/LoggerConfiguration.cs | 11 ++- .../Configuration/SetupController.cs | 3 +- Endpoint/Controllers/V1/AuthController.cs | 14 ++-- .../Controllers/V1/ProfessorController.cs | 2 +- Endpoint/Controllers/V1/ScheduleController.cs | 13 ++-- Endpoint/Controllers/V1/SecurityController.cs | 3 +- 10 files changed, 68 insertions(+), 74 deletions(-) delete mode 100644 ApiDto/Responses/ErrorResponse.cs diff --git a/ApiDto/Responses/ErrorResponse.cs b/ApiDto/Responses/ErrorResponse.cs deleted file mode 100644 index f0dc33c..0000000 --- a/ApiDto/Responses/ErrorResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Mirea.Api.Dto.Responses; - -/// -/// A class for providing information about an error -/// -public class ErrorResponse -{ - /// - /// The text or translation code of the error. This field may not contain information in specific scenarios. - /// For example, it might be empty for HTTP 204 responses where no content is returned or if the validation texts have not been configured. - /// - [Required] - public required string Error { get; set; } - /// - /// In addition to returning the response code in the header, it is also duplicated in this field. - /// Represents the HTTP response code. - /// - [Required] - public required int Code { get; set; } -} \ No newline at end of file diff --git a/Endpoint/Common/Attributes/BadRequestResponseAttribute.cs b/Endpoint/Common/Attributes/BadRequestResponseAttribute.cs index 85f9ed8..b424733 100644 --- a/Endpoint/Common/Attributes/BadRequestResponseAttribute.cs +++ b/Endpoint/Common/Attributes/BadRequestResponseAttribute.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Mirea.Api.Dto.Responses; using System; namespace Mirea.Api.Endpoint.Common.Attributes; [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] -public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status400BadRequest); \ No newline at end of file +public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status400BadRequest); \ No newline at end of file diff --git a/Endpoint/Common/Attributes/NotFoundResponseAttribute.cs b/Endpoint/Common/Attributes/NotFoundResponseAttribute.cs index 39527ea..7b5855f 100644 --- a/Endpoint/Common/Attributes/NotFoundResponseAttribute.cs +++ b/Endpoint/Common/Attributes/NotFoundResponseAttribute.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Mirea.Api.Dto.Responses; using System; namespace Mirea.Api.Endpoint.Common.Attributes; [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] -public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status404NotFound); \ No newline at end of file +public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status404NotFound); \ No newline at end of file diff --git a/Endpoint/Configuration/Core/Middleware/CustomExceptionHandlerMiddleware.cs b/Endpoint/Configuration/Core/Middleware/CustomExceptionHandlerMiddleware.cs index d5aff77..3c65dd8 100644 --- a/Endpoint/Configuration/Core/Middleware/CustomExceptionHandlerMiddleware.cs +++ b/Endpoint/Configuration/Core/Middleware/CustomExceptionHandlerMiddleware.cs @@ -1,10 +1,13 @@ using FluentValidation; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Mirea.Api.DataAccess.Application.Common.Exceptions; -using Mirea.Api.Dto.Responses; using Mirea.Api.Endpoint.Common.Exceptions; using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Security; using System.Text.Json; using System.Threading.Tasks; @@ -27,50 +30,55 @@ public class CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger() + { + { "traceId", traceId } + } + }; + switch (exception) { case ValidationException validationException: - code = StatusCodes.Status400BadRequest; - result = JsonSerializer.Serialize(new ErrorResponse() + problemDetails.Status = StatusCodes.Status400BadRequest; + problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1"; + problemDetails.Title = "Validation errors occurred."; + problemDetails.Extensions = new Dictionary { - Error = validationException.Message, - Code = code - }); + { "errors", validationException.Errors.Select(e => e.ErrorMessage).ToArray() }, + { "traceId", traceId } + }; break; case NotFoundException: - code = StatusCodes.Status404NotFound; + problemDetails.Status = StatusCodes.Status404NotFound; + problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.4"; + problemDetails.Title = "Resource not found."; break; case ControllerArgumentException: - code = StatusCodes.Status400BadRequest; + problemDetails.Status = StatusCodes.Status400BadRequest; + problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1"; + problemDetails.Title = "Invalid arguments provided."; break; case SecurityException: - code = StatusCodes.Status401Unauthorized; + problemDetails.Status = StatusCodes.Status401Unauthorized; + problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.2"; + problemDetails.Title = "Unauthorized access."; break; } - context.Response.ContentType = "application/json"; - context.Response.StatusCode = code; - - if (!string.IsNullOrEmpty(result)) - return context.Response.WriteAsync(result); - - string error; - if (code == StatusCodes.Status500InternalServerError) - { - error = "Internal Server Error"; + if (problemDetails.Status == StatusCodes.Status500InternalServerError) logger.LogError(exception, "Internal server error when processing the request"); - } - else - error = exception.Message; - result = JsonSerializer.Serialize(new ErrorResponse() - { - Error = error, - Code = code - }); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = problemDetails.Status.Value; - return context.Response.WriteAsync(result); + return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails)); } } \ No newline at end of file diff --git a/Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs b/Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs index da000b1..2e10259 100644 --- a/Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs +++ b/Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs @@ -4,9 +4,11 @@ using Microsoft.Extensions.Hosting; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Configuration.Model; using Serilog; +using Serilog.Context; using Serilog.Events; using Serilog.Filters; using Serilog.Formatting.Compact; +using System.Diagnostics; using System.IO; namespace Mirea.Api.Endpoint.Configuration.Core.Startup; @@ -55,7 +57,14 @@ public static class LoggerConfiguration public static IApplicationBuilder UseCustomSerilog(this IApplicationBuilder app) { - return app.UseSerilogRequestLogging(options => + return app.Use(async (context, next) => + { + var traceId = Activity.Current?.Id ?? context.TraceIdentifier; + using (LogContext.PushProperty("TraceId", traceId)) + { + await next(); + } + }).UseSerilogRequestLogging(options => { options.MessageTemplate = "[{RequestMethod}] {RequestPath} [Client {RemoteIPAddress}] [{StatusCode}] in {Elapsed:0.0000} ms"; diff --git a/Endpoint/Controllers/Configuration/SetupController.cs b/Endpoint/Controllers/Configuration/SetupController.cs index e1780f2..5ab4671 100644 --- a/Endpoint/Controllers/Configuration/SetupController.cs +++ b/Endpoint/Controllers/Configuration/SetupController.cs @@ -30,6 +30,7 @@ using System.IO; using System.Linq; using System.Net.Mail; using System.Runtime.InteropServices; +using System.Security; using System.Security.Cryptography; using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy; @@ -88,7 +89,7 @@ public class SetupController( } if (!setupToken.MatchToken(tokenBase64)) - return Unauthorized("The token is not valid"); + throw new SecurityException("The token is not valid"); Response.Cookies.Append(TokenAuthenticationAttribute.AuthToken, token, new CookieOptions { diff --git a/Endpoint/Controllers/V1/AuthController.cs b/Endpoint/Controllers/V1/AuthController.cs index 529b29a..3c26de0 100644 --- a/Endpoint/Controllers/V1/AuthController.cs +++ b/Endpoint/Controllers/V1/AuthController.cs @@ -15,6 +15,7 @@ using Mirea.Api.Security.Common.Domain; using Mirea.Api.Security.Services; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider; @@ -31,17 +32,17 @@ public class AuthController(IOptionsSnapshot user, IOptionsSnapshot"; + "'},'*');}window.close();}, 15000);"; - return $"{title}

{title}

{message}

Это информационная страница. Вы можете закрыть её.

{script}"; + return $"{title}

{title}

{message}

Это информационная страница. Вы можете закрыть её.

{(!string.IsNullOrEmpty(traceId) ? $"TraceId={traceId}" : string.Empty)}
{script}"; } [HttpGet("OAuth2")] @@ -56,6 +57,7 @@ public class AuthController(IOptionsSnapshot user, IOptionsSnapshot user, IOptionsSnapshot user, IOptionsSnapshot>> GetDetails(string name) { if (string.IsNullOrEmpty(name) || name.Length < 4) - return BadRequest($"The minimum number of characters is 4 (current: {name.Length})."); + throw new ControllerArgumentException($"The minimum number of characters is 4 (current: {name.Length})."); var result = await mediator.Send(new GetProfessorInfoSearchQuery() { diff --git a/Endpoint/Controllers/V1/ScheduleController.cs b/Endpoint/Controllers/V1/ScheduleController.cs index 55a7f03..00baf6e 100644 --- a/Endpoint/Controllers/V1/ScheduleController.cs +++ b/Endpoint/Controllers/V1/ScheduleController.cs @@ -8,6 +8,7 @@ using Mirea.Api.Dto.Common; using Mirea.Api.Dto.Requests; using Mirea.Api.Dto.Responses; using Mirea.Api.Endpoint.Common.Attributes; +using Mirea.Api.Endpoint.Common.Exceptions; using Mirea.Api.Endpoint.Common.MapperDto; using Mirea.Api.Endpoint.Configuration.Model; using System; @@ -52,14 +53,10 @@ public class ScheduleController(IMediator mediator, IOptionsSnapshot generalConfig) : } catch (Exception ex) { - return BadRequest($"Failed to generate QR code: {ex.Message}"); + throw new ControllerArgumentException($"Failed to generate QR code: {ex.Message}"); } }