user33276346
user33276346

Reputation: 1739

Format stack trace to manually return in API response

Currently it is returning a single unintelligible chunk composed of multiple of these things glued one next to the other like this:

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\\Repositories\\projectx\\src\\ProyectX.Services\\Service.cs:line 116\r\n at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\\Repositories\\projectx\\src\\ProyectX.Services\\Service.cs:line 116\r\n

Goal:

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\Repositories\projectx\src\ProyectX.Services\Service.cs:line 116

at ProyectX.Services.Service.Validate(IList 1 myParam) in C:\Repositories\projectx\src\ProyectX.Services\Service.cs:line 116

I tried with

Regex.Unescape(exception.StackTrace)

JsonSerializer.Serialize(exception.StackTrace, new JsonSerializerOptions() {WriteIndented = true });

The middleware is in Startup.cs

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<ErrorHandlerMiddleware>();

Middleweare:

using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using ProyectX.Domain.Exceptions;
using ProyectX.Domain.Models;

namespace ProyectX.API.Middleware
{
    public class ErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;

        public ErrorHandlerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context, IWebHostEnvironment env)
        {
            try
            {
                await _next(context);
            }
            catch (Exception exception)
            {
                var response = context.Response;
                response.ContentType = "application/json";

                switch (exception)
                {
                    case InvalidOperationException:
                        response.StatusCode = (int)HttpStatusCode.BadRequest;
                        break;
                    default:
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                }

                var details = new Dictionary<string, string>
                {
                    { "Message", exception.Message },
                };

                if (env.IsDevelopment())
                {
                    details.Add("StackTrace", exception.StackTrace);
                }

                var errorResponse = new ErrorResponseModel(exception, details);
                var result = JsonSerializer.Serialize(errorResponse);

                await response.WriteAsync(result);
            }
        }
    }
}

ErrorResponseModel

using System;
using System.Collections.Generic;

namespace ProyectX.Domain.Models
{
    public class ErrorResponseModel
    {
        public ErrorResponseModel(Exception ex, Dictionary<string, string> details)
        {
            Type = ex.GetType().Name;
            Details = details;
        }

        public string Type { get; set; }

        public IDictionary<string, string> Details { get; set; }
    }
}

Upvotes: 5

Views: 2862

Answers (2)

Maytham Fahmi
Maytham Fahmi

Reputation: 33427

I would suggest you make changes to your ErrorResponseModel class so that your dictionary accepts object type as value, like Dictionary<string, object>. Here comes the idea, with object type the dictionary value can accept adding string OR string array.

So for your StackTrace, you can split it into multiple lines using split by "\r\n" and get a string array of it, that I can pass to my StackTrace value.

So let's come to your model:

public class ErrorResponseModel
{
    public ErrorResponseModel(Exception ex, Dictionary<string, object> details)
    {
        Type = ex.GetType().Name;
        Details = details;
    }

    public string Type { get; set; }

    public IDictionary<string, object> Details { get; set; }
}

Here is the part that handles the exception:

var details = new Dictionary<string, object>
{
    { "Message", exception.Message} ,
};

var lines = exception.StackTrace?.Split("\r\n").Select(e => e.TrimStart());
details.Add("StackTrace", lines);

var errorResponse = new ErrorResponseModel(exception, details);

var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions() { WriteIndented = true });

All this returns the following:

enter image description here

Upvotes: 4

Reza Aghaei
Reza Aghaei

Reputation: 125247

Exception information model using StackTrace and StackFrame classes

There's a StackTrace class which can help you to get list of StackFrame objects. Each frame, includes information like:

  • Line Number
  • Method (Class, Assembly)
  • Code File

Looking into ASP.NET Core ErrorPage and DeveloperExceptionPageMiddleware you will see the framework has created a model class including the exception information like stack frame and then in the error page has formatted the model and showed in a readable format.

This is how Exception class also generates the StackTrace: by calling StackTrace.ToString which gets the stack frames and format above information in a string.

You can also do the same and generate the string in a better format, or as a better idea, create an exception or stacktrace model and let the formatting be done later.

For example, this is the exception model (including stacktrace) that you can have:

{
  "Message":"Attempted to divide by zero.",
  "StackFrames":[
    {
      "LineNumber":13,
      "Method":"Main(String[] args)",
      "Class":"SampleConsoleApp.Program",
      "AssemblyName":"SampleConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "AssemblyFile":"file:///C:/SampleConsoleApp/bin/Debug/netcoreapp3.1/SampleConsoleApp.dll",
      "CodeFile":"C:\\SampleConsoleApp\\Program.cs"
    },
    {
      "LineNumber":23,
      "Method":"Divide(Int32 x, Int32 y)",
      "Class":"SampleConsoleApp.Program",
      "AssemblyName":"SampleConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "AssemblyFile":"file:///C:/SampleConsoleApp/bin/Debug/netcoreapp3.1/SampleConsoleApp.dll",
      "CodeFile":"C:\\SampleConsoleApp\\Program.cs"
     }
  ],
  "InnerException":null
}

Above json string is created from my exception model:

try
{
}
catch (Exception ex)
{
    var model = ExceptionModel.Create(ex);

    // You can convert it to json and store it
    // Or format it and write to log 
}

ExceptionModel including StackTrace

Here is how I've implemented exception model:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
public class ExceptionModel
{
    public string Message { get; set; }
    public IEnumerable<StackFrameModel> StackFrames { get; set; }
    public ExceptionModel InnerException { get; set; }
    public static ExceptionModel Create(Exception ex = null)
    {
        var trace = ex == null ? new StackTrace(true) : new StackTrace(ex, true);
        var model = new ExceptionModel();
        model.Message = ex?.Message;
        model.StackFrames = trace.GetFrames().Reverse()
            .Select(x => new StackFrameModel()
        {
            LineNumber = x.GetFileLineNumber(),
            Method = GetMethodSignature(x.GetMethod()),
            Class = x.GetMethod()?.DeclaringType?.FullName,
            AssemblyName = x.GetMethod()?.DeclaringType?.Assembly?.FullName,
            AssemblyFile = x.GetMethod()?.DeclaringType?.Assembly?.CodeBase,
            CodeFile = x.GetFileName(),
        });
        if (ex?.InnerException != null)
            model.InnerException = ExceptionModel.Create(ex.InnerException);
        return model;
    }
    private static string GetTypeName(Type type)
    {
        return type?.FullName?.Replace('+', '.');
    }
    private static string GetMethodSignature(MethodBase mb)
    {
        var sb = new StringBuilder();
        sb.Append(mb.Name);

        // deal with the generic portion of the method
        if (mb is MethodInfo && ((MethodInfo)mb).IsGenericMethod)
        {
            Type[] typars = ((MethodInfo)mb).GetGenericArguments();
            sb.Append("[");
            int k = 0;
            bool fFirstTyParam = true;
            while (k < typars.Length)
            {
                if (fFirstTyParam == false)
                    sb.Append(",");
                else
                    fFirstTyParam = false;

                sb.Append(typars[k].Name);
                k++;
            }
            sb.Append("]");
        }

        // arguments printing
        sb.Append("(");
        ParameterInfo[] pi = mb.GetParameters();
        bool fFirstParam = true;
        for (int j = 0; j < pi.Length; j++)
        {
            if (fFirstParam == false)
                sb.Append(", ");
            else
                fFirstParam = false;

            String typeName = "<UnknownType>";
            if (pi[j].ParameterType != null)
                typeName = pi[j].ParameterType.Name;
            sb.Append(typeName + " " + pi[j].Name);
        }
        sb.Append(")");
        return sb.ToString();
    }
}
public class StackFrameModel
{
    public int LineNumber { get; set; }
    public string Method { get; set; }
    public string Class { get; set; }
    public string AssemblyName { get; set; }
    public string AssemblyFile { get; set; }
    public string CodeFile { get; set; }
}

Upvotes: 3

Related Questions