Introduction
I wanted to show my users some prettier errorpages then the default you get from IIS or Nancy.
This being the default for Nancy. Because I know you guys like pictures.
Of course this was the first time ever in any webapp that I did that, So I had to look certain things up. And I thought this was pretty cool.
But of course that’s not how Nancy works.
Nancy is a Lady, so we treat her like one.
Setting it up.
So first of we will have to change our webconfig so that it servers us custom errors.
You do this by adding the following.
<configuration>
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="PassThrough" />
</system.webServer>
</configuration>```
Next up is to make a few html file that we can serve as our custom pages.
Something like this.
```html
<!DOCTYPE html>
<html>
<head>
<title>404</title>
</head>
<body>
<p>Special Chrissie 404 page</p>
</body>
</html>
I made a similar one for 500.
I named them 404.html and 500.html and made sure the properties of the files were set to embedded resource.
Now I just had to create a custom IStatusCodeHandler, like this.
Imports System.IO
Imports Nancy
Imports Nancy.ErrorHandling
Public Class CustomErrors
Implements IStatusCodeHandler
Private ReadOnly _errorPages As IDictionary(Of HttpStatusCode, String)
Private ReadOnly _supportedStatusCodes As HttpStatusCode() = {HttpStatusCode.NotFound, HttpStatusCode.InternalServerError}
Public Sub New()
_errorPages = New Dictionary(Of HttpStatusCode, String) From
{
{HttpStatusCode.NotFound, LoadResource("404.html")},
{HttpStatusCode.InternalServerError, LoadResource("500.html")}
}
End Sub
Public Sub Handle(statusCode As HttpStatusCode, context As NancyContext) Implements IStatusCodeHandler.Handle
If context.Response IsNot Nothing AndAlso context.Response.Contents IsNot Nothing AndAlso Not ReferenceEquals(context.Response.Contents, Response.NoBody) Then
Return
End If
Dim errorPage As String
If Not _errorPages.TryGetValue(statusCode, errorPage) Then
Return
End If
If String.IsNullOrEmpty(errorPage) Then
Return
End If
ModifyResponse(statusCode, context, errorPage)
End Sub
Public Function HandlesStatusCode(statusCode As HttpStatusCode, context As NancyContext) As Boolean Implements IStatusCodeHandler.HandlesStatusCode
Return _supportedStatusCodes.Any(Function(s) s = statusCode)
End Function
Private Shared Sub ModifyResponse(statusCode As HttpStatusCode, context As NancyContext, errorPage As String)
If context.Response Is Nothing Then
context.Response = New Response() With {.StatusCode = statusCode}
End If
context.Response.ContentType = "text/html"
context.Response.Contents = Sub(s)
Using writer = New StreamWriter(s, Encoding.UTF8)
writer.Write(errorPage)
End Using
End Sub
End Sub
Private Shared Function LoadResource(filename As String) As String
Dim resourceStream = GetType(CustomErrors).Assembly.GetManifestResourceStream(String.Format("WebApplication3.{0}", filename))
If resourceStream Is Nothing Then
Return String.Empty
End If
Using reader = New StreamReader(resourceStream)
Return reader.ReadToEnd()
End Using
End Function
End Class
Normally one would expect Nancy to pick this file up, because Nancy is a good girl. (One of the many benefits of giving your framework a woman’s name is all the nice cliches bloggers can use, Thank you for that) (One of the disadvantages is that Google has a dirty mind.)
But Nancy has a bug, so you have to register it yourself. You can do this in the bootstrapper. I logged the bug in the github issuetracker so this might be resolved by the time you want to use this. For your information, this bug was fixed 3 hours after logging it.
Imports Nancy.Bootstrapper
Public Class MyBootStrapper
Inherits Nancy.DefaultNancyBootstrapper
Protected Overrides ReadOnly Property InternalConfiguration As NancyInternalConfiguration
Get
Return NancyInternalConfiguration.WithOverrides(Sub(b)
b.StatusCodeHandlers = New List(Of Type) From {GetType(CustomErrors)}
End Sub)
End Get
End Property
End Class
I’m sure I killed a few puppies along the way. But it works. And here is the smoking gun.
Pretty, no?
I have to thank Steven Robbins and Phillip Haydon for their kind and generous help.
And you can make it even better by using a masterpage.
Just create a Master.html.
<!DOCTYPE html>
<html>
<head>
<title>[Title]</title>
<link href="/Content/Css/main.css" rel="stylesheet" />
</head>
<body>
[Details]
</body>
</html>```
And see how I reference the css in my content folder.
And then you can adapt the 404 and 500 page.
```html
<h2>Page not found</h2>
<p>We could not find the page you were looking for. Are you sure this is where you left it? This error is logged but I'm pretty sure the administrator can not fix it.</p>
<p>If it is urgent you can contact you admin by shouting real loud.</p>
<p>BTW: You can find a link in the footer to the logfiles if you want to fix the error yourself.</p>
Now we go back to our CustomErrors class. And change the LoadResource method to this.
vbnet
Private Shared Function LoadResource(ByVal filename As String, ByVal httpStatusCode As HttpStatusCode) As String
Dim masterPageStream = GetType(CustomErrors).Assembly.GetManifestResourceStream(String.Format("BeCare_Server.{0}", "Master.html"))
Dim masterPage As String
Using reader = New StreamReader(masterPageStream)
masterPage = reader.ReadToEnd()
End Using
masterPage = masterPage.Replace("[Title]", String.Format("Error {0}", httpStatusCode.ToString("D")))
Dim resourceStream = GetType(CustomErrors).Assembly.GetManifestResourceStream(String.Format("BeCare_Server.{0}", filename))
If resourceStream Is Nothing Then
Return String.Empty
End If
Dim details As String
Using reader = New StreamReader(resourceStream)
details = reader.ReadToEnd()
End Using
masterPage = masterPage.Replace("[Details]", details)
Return masterPage
End Function
And you can change your Handle method to this.
```vbnet Public Sub Handle(statusCode As HttpStatusCode, context As NancyContext) Implements IStatusCodeHandler.Handle If context.Response IsNot Nothing AndAlso context.Response.Contents IsNot Nothing AndAlso Not ReferenceEquals(context.Response.Contents, Response.NoBody) Then Return End If
Dim errorPage As String
If Not _errorPages.TryGetValue(statusCode, errorPage) Then
Return
End If
If String.IsNullOrEmpty(errorPage) Then
errorPage = LoadResource("500.html", statusCode)
End If
ModifyResponse(statusCode, context, errorPage)
End Sub```
So that now every statuscode in the known world is handled by your custom errorpages. Of course this is optional.
And now you get pretty page.
Cooooooooooollll!!!