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!!!