Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The Octopus Nancy error handling strategy
protected override void RequestStartup(ILifetimeScope requestContainer, IPipelines pipelines, NancyContext context)
{
pipelines.OnError.AddItemToEndOfPipeline((z, a) =>
{
log.Error("Unhandled error on request: " + context.Request.Url + " : " + a.Message, a);
return ErrorResponse.FromException(a);
});
base.RequestStartup(requestContainer, pipelines, context);
}
public class ErrorHtmlPageResponse : HtmlResponse
{
static readonly Regex ReplacementTokenRegex = new Regex("\\#\\{(?<variable>.+?)\\}", RegexOptions.Compiled | RegexOptions.Singleline);
static readonly string ErrorTemplate;
static ErrorHtmlPageResponse()
{
var stream = typeof (ErrorStatusCodeHandler).Assembly.GetManifestResourceStream(typeof (ErrorStatusCodeHandler).Namespace + ".Error.html");
using (var reader = new StreamReader(stream))
{
ErrorTemplate = reader.ReadToEnd();
}
}
public ErrorHtmlPageResponse(HttpStatusCode statusCode)
{
StatusCode = statusCode;
ContentType = "text/html; charset=utf-8";
Contents = Render;
}
public string Title { get; set; }
public string Summary { get; set; }
public string Details { get; set; }
void Render(Stream stream)
{
var formatArguments = GetErrorPageDetails();
var page = ReplacementTokenRegex.Replace(ErrorTemplate, match =>
{
string value;
return formatArguments.TryGetValue(match.Groups["variable"].Value, out value) ? value : string.Empty;
});
using (var writer = new StreamWriter(stream))
{
writer.WriteLine(page);
writer.Flush();
}
}
Dictionary<string, string> GetErrorPageDetails()
{
var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
parameters["ErrorTitle"] = Title;
parameters["ErrorSummary"] = Summary;
if (!string.IsNullOrWhiteSpace(Details))
{
parameters["ErrorDetails"] = "<h3>Details</h3><pre>" + Details + "</pre>";
}
parameters["EmailSubject"] = "Error from Octopus: " + Summary;
return parameters;
}
}
public class ErrorResponse : JsonResponse
{
readonly Error error;
private ErrorResponse(Error error)
: base(error, new CustomJsonSerializer())
{
Guard.ArgumentNotNull(error, "error");
this.error = error;
}
public string ErrorMessage { get { return error.ErrorMessage; } }
public string FullException { get { return error.FullException; } }
public string[] Errors { get { return error.Errors; } }
public static ErrorResponse FromMessage(string message)
{
return new ErrorResponse(new Error { ErrorMessage = message });
}
public static ErrorResponse FromException(Exception ex)
{
var exception = ex.GetRootError();
var summary = exception.Message;
if (exception is WebException || exception is SocketException)
{
// Commonly returned when connections to RavenDB fail
summary = "The Octopus windows service may not be running: " + summary;
}
var statusCode = HttpStatusCode.InternalServerError;
var error = new Error { ErrorMessage = summary, FullException = exception.ToString() };
// Special cases
if (exception is ResourceNotFoundException)
{
statusCode = HttpStatusCode.NotFound;
error.FullException = null;
}
if (exception is OctopusSecurityException)
{
statusCode = HttpStatusCode.Forbidden;
error.FullException = null;
}
var response = new ErrorResponse(error);
response.StatusCode = statusCode;
return response;
}
class Error
{
public string ErrorMessage { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string FullException { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string[] Errors { get; set; }
}
}
public sealed class ErrorStatusCodeHandler : IStatusCodeHandler
{
public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context)
{
return statusCode == HttpStatusCode.NotFound
|| statusCode == HttpStatusCode.InternalServerError
|| statusCode == HttpStatusCode.Forbidden
|| statusCode == HttpStatusCode.Unauthorized;
}
public void Handle(HttpStatusCode statusCode, NancyContext context)
{
var clientWantsHtml = ShouldRenderFriendlyErrorPage(context);
if (!clientWantsHtml)
{
if (context.Response is NotFoundResponse)
{
// Normally we return 404's ourselves so we have an ErrorResponse.
// But if no route is matched, Nancy will set a NotFound response itself.
// When this happens we still want to return our nice JSON response.
context.Response = ErrorResponse.FromMessage("The resource you requested was not found.").WithStatusCode(statusCode);
}
// Pass the existing response through
return;
}
var error = context.Response as ErrorResponse;
switch (statusCode)
{
case HttpStatusCode.Unauthorized:
context.Response = new RedirectResponse(WebRoutes.Web.Accounts.Login());
break;
case HttpStatusCode.Forbidden:
context.Response = new ErrorHtmlPageResponse(statusCode)
{
Title = "Permission",
Summary = error == null ? "Sorry, you do not have permission to perform that action. Please contact your Octopus administrator." : error.ErrorMessage
};
break;
case HttpStatusCode.NotFound:
context.Response = new ErrorHtmlPageResponse(statusCode)
{
Title = "404 Not found",
Summary = "Sorry, the resource you requested was not found."
};
break;
case HttpStatusCode.InternalServerError:
context.Response = new ErrorHtmlPageResponse(statusCode)
{
Title = "Sorry, something went wrong",
Summary = error == null ? "An unexpected error occurred." : error.ErrorMessage,
Details = error == null ? null : error.FullException
};
break;
}
}
static bool ShouldRenderFriendlyErrorPage(NancyContext context)
{
var enumerable = context.Request.Headers.Accept;
var ranges = enumerable.OrderByDescending(o => o.Item2).Select(o => MediaRange.FromString(o.Item1)).ToList();
foreach (var item in ranges)
{
if (item.Matches("application/json"))
return false;
if (item.Matches("text/json"))
return false;
if (item.Matches("text/html"))
return true;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment