361 lines
13 KiB
C#
361 lines
13 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Diagnostics.Views;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.FileProviders;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StackFrame = Microsoft.AspNetCore.Diagnostics.Views.StackFrame;
|
|
|
|
namespace Microsoft.AspNetCore.Diagnostics
|
|
{
|
|
/// <summary>
|
|
/// Captures synchronous and asynchronous exceptions from the pipeline and generates HTML error responses.
|
|
/// </summary>
|
|
public class DeveloperExceptionPageMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly DeveloperExceptionPageOptions _options;
|
|
private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null;
|
|
private readonly ILogger _logger;
|
|
private readonly IFileProvider _fileProvider;
|
|
private readonly DiagnosticSource _diagnosticSource;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DeveloperExceptionPageMiddleware"/> class
|
|
/// </summary>
|
|
/// <param name="next"></param>
|
|
/// <param name="options"></param>
|
|
/// <param name="loggerFactory"></param>
|
|
/// <param name="hostingEnvironment"></param>
|
|
/// <param name="diagnosticSource"></param>
|
|
public DeveloperExceptionPageMiddleware(
|
|
RequestDelegate next,
|
|
IOptions<DeveloperExceptionPageOptions> options,
|
|
ILoggerFactory loggerFactory,
|
|
IHostingEnvironment hostingEnvironment,
|
|
DiagnosticSource diagnosticSource)
|
|
{
|
|
if (next == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(next));
|
|
}
|
|
|
|
if (options == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
_next = next;
|
|
_options = options.Value;
|
|
_logger = loggerFactory.CreateLogger<DeveloperExceptionPageMiddleware>();
|
|
_fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider;
|
|
_diagnosticSource = diagnosticSource;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process an individual request.
|
|
/// </summary>
|
|
/// <param name="context"></param>
|
|
/// <returns></returns>
|
|
public async Task Invoke(HttpContext context)
|
|
{
|
|
try
|
|
{
|
|
await _next(context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(0, ex, "An unhandled exception has occurred while executing the request");
|
|
|
|
if (context.Response.HasStarted)
|
|
{
|
|
_logger.LogWarning("The response has already started, the error page middleware will not be executed.");
|
|
throw;
|
|
}
|
|
|
|
try
|
|
{
|
|
context.Response.Clear();
|
|
context.Response.StatusCode = 500;
|
|
|
|
await DisplayException(context, ex);
|
|
|
|
if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException"))
|
|
{
|
|
_diagnosticSource.Write("Microsoft.AspNetCore.Diagnostics.UnhandledException", new { httpContext = context, exception = ex });
|
|
}
|
|
|
|
return;
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
// If there's a Exception while generating the error page, re-throw the original exception.
|
|
_logger.LogError(0, ex2, "An exception was thrown attempting to display the error page.");
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Assumes the response headers have not been sent. If they have, still attempt to write to the body.
|
|
private Task DisplayException(HttpContext context, Exception ex)
|
|
{
|
|
var compilationException = ex as ICompilationException;
|
|
if (compilationException != null)
|
|
{
|
|
return DisplayCompilationException(context, compilationException);
|
|
}
|
|
|
|
return DisplayRuntimeException(context, ex);
|
|
}
|
|
|
|
private Task DisplayCompilationException(
|
|
HttpContext context,
|
|
ICompilationException compilationException)
|
|
{
|
|
var model = new CompilationErrorPageModel
|
|
{
|
|
Options = _options,
|
|
};
|
|
|
|
foreach (var compilationFailure in compilationException.CompilationFailures)
|
|
{
|
|
var stackFrames = new List<StackFrame>();
|
|
var errorDetails = new ErrorDetails
|
|
{
|
|
StackFrames = stackFrames
|
|
};
|
|
var fileContent = compilationFailure
|
|
.SourceFileContent
|
|
.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
|
|
|
foreach (var item in compilationFailure.Messages)
|
|
{
|
|
var frame = new StackFrame
|
|
{
|
|
File = compilationFailure.SourceFilePath,
|
|
Line = item.StartLine,
|
|
Function = string.Empty
|
|
};
|
|
|
|
ReadFrameContent(frame, fileContent, item.StartLine, item.EndLine);
|
|
frame.ErrorDetails = item.Message;
|
|
|
|
stackFrames.Add(frame);
|
|
}
|
|
|
|
model.ErrorDetails.Add(errorDetails);
|
|
}
|
|
|
|
var errorPage = new CompilationErrorPage
|
|
{
|
|
Model = model
|
|
};
|
|
|
|
return errorPage.ExecuteAsync(context);
|
|
}
|
|
|
|
private Task DisplayRuntimeException(HttpContext context, Exception ex)
|
|
{
|
|
var request = context.Request;
|
|
|
|
var model = new ErrorPageModel
|
|
{
|
|
Options = _options,
|
|
ErrorDetails = GetErrorDetails(ex).Reverse(),
|
|
Query = request.Query,
|
|
Cookies = request.Cookies,
|
|
Headers = request.Headers
|
|
};
|
|
|
|
var errorPage = new ErrorPage(model);
|
|
return errorPage.ExecuteAsync(context);
|
|
}
|
|
|
|
private IEnumerable<ErrorDetails> GetErrorDetails(Exception ex)
|
|
{
|
|
for (var scan = ex; scan != null; scan = scan.InnerException)
|
|
{
|
|
yield return new ErrorDetails
|
|
{
|
|
Error = scan,
|
|
StackFrames = StackFrames(scan)
|
|
};
|
|
}
|
|
}
|
|
|
|
private IEnumerable<StackFrame> StackFrames(Exception ex)
|
|
{
|
|
var stackTrace = ex.StackTrace;
|
|
if (!string.IsNullOrEmpty(stackTrace))
|
|
{
|
|
var heap = new Chunk { Text = stackTrace + Environment.NewLine, End = stackTrace.Length + Environment.NewLine.Length };
|
|
for (var line = heap.Advance(Environment.NewLine); line.HasValue; line = heap.Advance(Environment.NewLine))
|
|
{
|
|
yield return StackFrame(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
private StackFrame StackFrame(Chunk line)
|
|
{
|
|
line.Advance(" at ");
|
|
string function = line.Advance(" in ").ToString();
|
|
|
|
//exception message line format differences in .net and mono
|
|
//On .net : at ConsoleApplication.Program.Main(String[] args) in D:\Program.cs:line 16
|
|
//On Mono : at ConsoleApplication.Program.Main(String[] args) in d:\Program.cs:16
|
|
string file = !IsMono ?
|
|
line.Advance(":line ").ToString() :
|
|
line.Advance(":").ToString();
|
|
|
|
int lineNumber = line.ToInt32();
|
|
|
|
if (string.IsNullOrEmpty(file))
|
|
{
|
|
return GetStackFrame(
|
|
// Handle stack trace lines like
|
|
// "--- End of stack trace from previous location where exception from thrown ---"
|
|
string.IsNullOrEmpty(function) ? line.ToString() : function,
|
|
file: string.Empty,
|
|
lineNumber: 0);
|
|
}
|
|
else
|
|
{
|
|
return GetStackFrame(function, file, lineNumber);
|
|
}
|
|
}
|
|
|
|
// make it internal to enable unit testing
|
|
internal StackFrame GetStackFrame(string function, string file, int lineNumber)
|
|
{
|
|
var frame = new StackFrame { Function = function, File = file, Line = lineNumber };
|
|
|
|
if (string.IsNullOrEmpty(file))
|
|
{
|
|
return frame;
|
|
}
|
|
|
|
IEnumerable<string> lines = null;
|
|
if (File.Exists(file))
|
|
{
|
|
lines = File.ReadLines(file);
|
|
}
|
|
else
|
|
{
|
|
// Handle relative paths and embedded files
|
|
var fileInfo = _fileProvider.GetFileInfo(file);
|
|
if (fileInfo.Exists)
|
|
{
|
|
// ReadLines doesn't accept a stream. Use ReadLines as its more efficient
|
|
// relative to reading lines via stream reader
|
|
if (!string.IsNullOrEmpty(fileInfo.PhysicalPath))
|
|
{
|
|
lines = File.ReadLines(fileInfo.PhysicalPath);
|
|
}
|
|
else
|
|
{
|
|
lines = ReadLines(fileInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lines != null)
|
|
{
|
|
ReadFrameContent(frame, lines, lineNumber, lineNumber);
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
// make it internal to enable unit testing
|
|
internal void ReadFrameContent(
|
|
StackFrame frame,
|
|
IEnumerable<string> allLines,
|
|
int errorStartLineNumberInFile,
|
|
int errorEndLineNumberInFile)
|
|
{
|
|
// Get the line boundaries in the file to be read and read all these lines at once into an array.
|
|
var preErrorLineNumberInFile = Math.Max(errorStartLineNumberInFile - _options.SourceCodeLineCount, 1);
|
|
var postErrorLineNumberInFile = errorEndLineNumberInFile + _options.SourceCodeLineCount;
|
|
var codeBlock = allLines
|
|
.Skip(preErrorLineNumberInFile - 1)
|
|
.Take(postErrorLineNumberInFile - preErrorLineNumberInFile + 1)
|
|
.ToArray();
|
|
|
|
var numOfErrorLines = (errorEndLineNumberInFile - errorStartLineNumberInFile) + 1;
|
|
var errorStartLineNumberInArray = errorStartLineNumberInFile - preErrorLineNumberInFile;
|
|
|
|
frame.PreContextLine = preErrorLineNumberInFile;
|
|
frame.PreContextCode = codeBlock.Take(errorStartLineNumberInArray).ToArray();
|
|
frame.ContextCode = codeBlock
|
|
.Skip(errorStartLineNumberInArray)
|
|
.Take(numOfErrorLines)
|
|
.ToArray();
|
|
frame.PostContextCode = codeBlock
|
|
.Skip(errorStartLineNumberInArray + numOfErrorLines)
|
|
.ToArray();
|
|
}
|
|
|
|
private static IEnumerable<string> ReadLines(IFileInfo fileInfo)
|
|
{
|
|
using (var reader = new StreamReader(fileInfo.CreateReadStream()))
|
|
{
|
|
string line;
|
|
while ((line = reader.ReadLine()) != null)
|
|
{
|
|
yield return line;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class Chunk
|
|
{
|
|
public string Text { get; set; }
|
|
public int Start { get; set; }
|
|
public int End { get; set; }
|
|
|
|
public bool HasValue => Text != null;
|
|
|
|
public Chunk Advance(string delimiter)
|
|
{
|
|
int indexOf = HasValue ? Text.IndexOf(delimiter, Start, End - Start, StringComparison.Ordinal) : -1;
|
|
if (indexOf < 0)
|
|
{
|
|
return new Chunk();
|
|
}
|
|
|
|
var chunk = new Chunk { Text = Text, Start = Start, End = indexOf };
|
|
Start = indexOf + delimiter.Length;
|
|
return chunk;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return HasValue ? Text.Substring(Start, End - Start) : string.Empty;
|
|
}
|
|
|
|
public int ToInt32()
|
|
{
|
|
int value;
|
|
return HasValue && int.TryParse(
|
|
Text.Substring(Start, End - Start),
|
|
NumberStyles.Integer,
|
|
CultureInfo.InvariantCulture,
|
|
out value) ? value : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|