Created
May 9, 2013 10:03
-
-
Save PaulStovell/5546666 to your computer and use it in GitHub Desktop.
The Octopus Nancy error handling strategy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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