From d2892fc799604cd1261b9c943edf46cea4f06845 Mon Sep 17 00:00:00 2001
From: Matthew Holt <Matthew.Holt+git@gmail.com>
Date: Sat, 28 Mar 2015 16:50:06 -0600
Subject: [PATCH] New error handler middleware

---
 middleware/errors/errors.go | 168 ++++++++++++++++++++++++++++++++++++
 1 file changed, 168 insertions(+)
 create mode 100644 middleware/errors/errors.go

diff --git a/middleware/errors/errors.go b/middleware/errors/errors.go
new file mode 100644
index 000000000..506f36082
--- /dev/null
+++ b/middleware/errors/errors.go
@@ -0,0 +1,168 @@
+// Package errors implements an HTTP error handling middleware.
+package errors
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"strconv"
+
+	"github.com/mholt/caddy/middleware"
+)
+
+// New instantiates a new instance of error-handling middleware.
+func New(c middleware.Controller) (middleware.Middleware, error) {
+	handler, err := parse(c)
+	if err != nil {
+		return nil, err
+	}
+
+	// Open the log file for writing when the server starts
+	c.Startup(func() error {
+		var err error
+		var file *os.File
+
+		if handler.LogFile == "stdout" {
+			file = os.Stdout
+		} else if handler.LogFile == "stderr" {
+			file = os.Stderr
+		} else {
+			file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
+			if err != nil {
+				return err
+			}
+		}
+
+		handler.Log = log.New(file, "", 0)
+		return nil
+	})
+
+	return func(next middleware.HandlerFunc) middleware.HandlerFunc {
+		handler.Next = next
+		return handler.ServeHTTP
+	}, nil
+}
+
+// ErrorHandler handles HTTP errors (or errors from other middleware).
+type ErrorHandler struct {
+	Next       middleware.HandlerFunc
+	ErrorPages map[int]string // map of status code to filename
+	LogFile    string
+	Log        *log.Logger
+}
+
+func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+	defer func() {
+		if rec := recover(); rec != nil {
+			h.Log.Printf("[PANIC %s] %v", r.URL.String(), rec)
+			h.errorPage(w, http.StatusInternalServerError)
+		}
+	}()
+
+	status, err := h.Next(w, r)
+
+	if err != nil {
+		h.Log.Printf("[ERROR %d %s] %v", status, r.URL.Path, err)
+	}
+
+	if status >= 400 {
+		h.errorPage(w, status)
+	}
+
+	return status, err
+}
+
+// errorPage serves a static error page to w according to the status
+// code. If there is an error serving the error page, a plaintext error
+// message is written instead, and the extra error is logged.
+func (h ErrorHandler) errorPage(w http.ResponseWriter, code int) {
+	defaultBody := fmt.Sprintf("%d %s", code, http.StatusText(code))
+
+	// See if an error page for this status code was specified
+	if pagePath, ok := h.ErrorPages[code]; ok {
+
+		// Try to open it
+		errorPage, err := os.Open(pagePath)
+		if err != nil {
+			// An error handling an error... <insert grumpy cat here>
+			h.Log.Printf("HTTP %d could not load error page %s: %v", code, pagePath, err)
+			http.Error(w, defaultBody, code)
+			return
+		}
+		defer errorPage.Close()
+
+		// Copy the page body into the response
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		w.WriteHeader(code)
+		_, err = io.Copy(w, errorPage)
+		if err != nil {
+			// Epic fail... sigh.
+			h.Log.Printf("HTTP %d could not respond with %s: %v", code, pagePath, err)
+			http.Error(w, defaultBody, code)
+		}
+
+		return
+	}
+
+	// Default error response
+	http.Error(w, defaultBody, code)
+}
+
+func parse(c middleware.Controller) (ErrorHandler, error) {
+	handler := ErrorHandler{ErrorPages: make(map[int]string)}
+
+	optionalBlock := func() (bool, error) {
+		var hadBlock bool
+
+		for c.NextBlock() {
+			hadBlock = true
+
+			what := c.Val()
+			if !c.NextArg() {
+				return hadBlock, c.ArgErr()
+			}
+			where := c.Val()
+
+			if what == "log" {
+				handler.LogFile = where
+			} else {
+				// Error page; ensure it exists
+				f, err := os.Open(where)
+				if err != nil {
+					return hadBlock, c.Err("Unable to open error page '" + where + "': " + err.Error())
+				}
+				f.Close()
+
+				whatInt, err := strconv.Atoi(what)
+				if err != nil {
+					return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
+				}
+				handler.ErrorPages[whatInt] = where
+			}
+		}
+		return hadBlock, nil
+	}
+
+	for c.Next() {
+		// Configuration may be in a block
+		hadBlock, err := optionalBlock()
+		if err != nil {
+			return handler, err
+		}
+
+		// Otherwise, the only argument would be an error log file name
+		if !hadBlock {
+			if c.NextArg() {
+				handler.LogFile = c.Val()
+			} else {
+				handler.LogFile = defaultLogFilename
+			}
+		}
+	}
+
+	return handler, nil
+}
+
+const defaultLogFilename = "error.log"