diff --git a/README.md b/README.md
index 68c2b685db62c628e738fcf17d0b4c0f4e4b4615..f6b2312b87b4b2d0441cffabaafe58cce6f58d20 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,12 @@
 Simple but quick Golang webserver that serves requests to get files and
 redirects from [PostgreSQL](https://www.postgresql.org).
 
+### Features
+- Serves files, short URLs and "tombstones" (deleted file markers)
+- Allows for URL previewing on short URLs (add `?preview`)
+- Allows for thumbnail generation on images (if enabled, add `?thumbnail`)
+- Can be configured to store generalized metrics
+
 ### Requirements
 
 - PostgreSQL server with `objects` table
@@ -40,11 +46,10 @@ means using the `.json` files in [lib/metrics/](lib/metrics).
 
 ### TODO
 
-- [ ] Write tests
-- [ ] Add thumbnail functionality
+- `OPTIONS`/`HEAD` support
+- Write tests
 
 ### License
 
 `cdn-origin` is licensed under the MIT license. A copy of the MIT license can be
 found in [LICENSE](LICENSE).
-
diff --git a/config.sample.toml b/config.sample.toml
index fc406e36d797339dbdcc7746713de06abd0f28c9..79c163165f8c679b07eb1ccc25ab82da9e3ca19f 100644
--- a/config.sample.toml
+++ b/config.sample.toml
@@ -56,3 +56,13 @@
 [files]
     # Storage location of the bucket on disk
     storageLocation = "/var/data/buckets/public"
+
+[thumbnails]
+    # Enable thumbnails? (add ?thumbnail to the end of a file object URL)
+    enable = true
+
+    # Enable thumbnail cache?
+    cacheEnable = true
+
+    # Thumbnail cache location (if enabled).
+    cacheLocation = "/tmp/thumbs"
diff --git a/go.mod b/go.mod
index 0b3a6b2aa08477bdc212bfc113c03a64b73e7cbf..1f55c5fd194a7db46627adb4caefdcba9ef7741f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,10 @@
 module owo.codes/whats-this/cdn-origin
 
 require (
+	github.com/discordapp/lilliput v0.0.0-20190215194814-3d66dff0ee01
 	github.com/lib/pq v1.0.0
 	github.com/oschwald/maxminddb-golang v1.3.0
+	github.com/pkg/errors v0.8.0
 	github.com/rs/zerolog v1.11.0
 	github.com/spf13/pflag v1.0.3
 	github.com/spf13/viper v1.3.1
diff --git a/go.sum b/go.sum
index ba1987215e68bd3c29a3289967f8193fd166a701..7e79b129c1835cd29abde41b7452fd10ca3c1050 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,8 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/discordapp/lilliput v0.0.0-20190215194814-3d66dff0ee01 h1:8iBDqApWovRWN5H5XG9RbaU2Jk0SJ9oPWS8yRwJqkrU=
+github.com/discordapp/lilliput v0.0.0-20190215194814-3d66dff0ee01/go.mod h1:Y/qDoFO8ipcjzUjR+FB3NrX2oJcp6EXQBLuW0nws2IY=
 github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
diff --git a/lib/db/models.go b/lib/db/models.go
index 46e7ec30adeb3e06bee6559a760657c27b2f346b..544c7026aecbe97f3ed8ebe19cd4d3f008f476ac 100644
--- a/lib/db/models.go
+++ b/lib/db/models.go
@@ -11,4 +11,5 @@ type Object struct {
 	ObjectType   int        `json:"object_type"`
 	DeletedAt    *time.Time `json:"deleted_at"`
 	DeleteReason *string    `json:"delete_reason"`
+	MD5Hash      *string    `json:"md5_hash"`
 }
diff --git a/lib/db/queries.go b/lib/db/queries.go
index 4fb6abf8aebe68ef81d3fab8a3b57d383ee2faef..6b6bfeb5de06833984c02ef5bf068e572812b293 100644
--- a/lib/db/queries.go
+++ b/lib/db/queries.go
@@ -16,8 +16,9 @@ func SelectObjectByBucketKey(bucket, key string) (Object, error) {
 	var objectType int
 	var deletedAt pq.NullTime
 	var deleteReason sql.NullString
+	var md5Hash sql.NullString
 	err := DB.QueryRow(selectObjectByBucketKey, fmt.Sprintf("%s/%s", bucket, key)).
-		Scan(&contentType, &destURL, &objectType, &deletedAt, &deleteReason)
+		Scan(&contentType, &destURL, &objectType, &deletedAt, &deleteReason, &md5Hash)
 	if err != nil {
 		return object, err
 	}
@@ -27,7 +28,7 @@ func SelectObjectByBucketKey(bucket, key string) (Object, error) {
 		object.ContentType = &contentType.String
 	}
 	if destURL.Valid {
-		object.DestURL= &destURL.String
+		object.DestURL = &destURL.String
 	}
 	if deletedAt.Valid {
 		object.DeletedAt = &deletedAt.Time
@@ -35,6 +36,9 @@ func SelectObjectByBucketKey(bucket, key string) (Object, error) {
 			object.DeleteReason = &deleteReason.String
 		}
 	}
+	if md5Hash.Valid {
+		object.MD5Hash = &md5Hash.String
+	}
 	object.ObjectType = objectType
 	return object, nil
 }
diff --git a/lib/db/sql.go b/lib/db/sql.go
index 22e49a31f567a63c7a63bbb8667562b2b018f37a..0b43b14e75401cc2afc5cd0853edfda0e126f246 100644
--- a/lib/db/sql.go
+++ b/lib/db/sql.go
@@ -6,7 +6,8 @@ SELECT
 	dest_url,
 	"type",
 	deleted_at,
-	delete_reason
+	delete_reason,
+	md5_hash
 FROM
 	objects
 WHERE
diff --git a/lib/thumbnailer/cache.go b/lib/thumbnailer/cache.go
new file mode 100644
index 0000000000000000000000000000000000000000..cc323e4656bff0175ec05476f7c47441e36b0aa6
--- /dev/null
+++ b/lib/thumbnailer/cache.go
@@ -0,0 +1,59 @@
+package thumbnailer
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+)
+
+// ThumbnailCache allows access to thumbnails stored in a directory. Each
+// thumbnail has a key, which uniquely identifies it. The key should be a unique
+// ID from a database or the original file's hash.
+type ThumbnailCache struct {
+	Directory string
+}
+
+// NewThumbnailCache creates a new *ThumbnailCache.
+func NewThumbnailCache(directory string) *ThumbnailCache {
+	return &ThumbnailCache{
+		Directory: directory,
+	}
+}
+
+// GetThumbnail returns a thumbnail that is cached. If no cached copy exists, a
+// exists, a NoCachedCopy error is returned.
+func (c *ThumbnailCache) GetThumbnail(key string) (io.ReadCloser, error) {
+	path := filepath.Join(c.Directory, key)
+	data, err := os.Open(path)
+	if os.IsNotExist(err) {
+		return nil, NoCachedCopy
+	}
+	return data, err
+}
+
+// SetThumbnail stores a thumbnail with the specified key.
+func (c *ThumbnailCache) SetThumbnail(key string, data io.Reader) error {
+	path := filepath.Join(c.Directory, key)
+	file, err := os.Create(path)
+	defer file.Close()
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(file, data)
+	return err
+}
+
+// Transform generates a thumbnail and caches it.
+func (c *ThumbnailCache) Transform(key string, data io.Reader) error {
+	outputImage, err := Transform(data)
+	if err != nil {
+		return err
+	}
+	return c.SetThumbnail(key, outputImage)
+}
+
+// DeleteThumbnail deletes a thumbnail from the cache.
+func (c *ThumbnailCache) DeleteThumbnail(key string) error {
+	path := filepath.Join(c.Directory, key)
+	return os.Remove(path)
+}
diff --git a/lib/thumbnailer/errors.go b/lib/thumbnailer/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..2c655c643c445915fef7ba6707450dcd5c6009c7
--- /dev/null
+++ b/lib/thumbnailer/errors.go
@@ -0,0 +1,16 @@
+package thumbnailer
+
+type thumbnailerError struct {
+	Err string
+}
+
+// Error implements error.
+func (e *thumbnailerError) Error() string {
+	return e.Err
+}
+
+// NoCachedCopy means there is no cached copy for the specified key available.
+var NoCachedCopy error = &thumbnailerError{"no cached copy of the thumbnail requested is available"}
+
+// InputTooLarge means that the pixel size of the input image is too big to be thumbnailed.
+var InputTooLarge error = &thumbnailerError{"the input size in pixels is too large"}
diff --git a/lib/thumbnailer/transform.go b/lib/thumbnailer/transform.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab34b53f61ae6c07abc36a45b2b9855dee3f179c
--- /dev/null
+++ b/lib/thumbnailer/transform.go
@@ -0,0 +1,117 @@
+package thumbnailer
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"sync"
+
+	"github.com/discordapp/lilliput"
+	"github.com/pkg/errors"
+)
+
+// Transform settings. These should remain constant. If these are ever changed,
+// the thumbnail cache should be cleared.
+var (
+	maxInputResolution  = 10000           // maximum length in pixels of any input length
+	maxOutputResolution = 200             // maximum length in pixels of any output length
+	outputBufferSize    = 5 * 1024 * 1024 // bytes
+	outputType          = ".jpeg"         // with leading dot
+	encodeOptions       = map[int]int{
+		lilliput.JpegQuality: 85,
+	}
+)
+
+// Accepted MIME types for thumbnails in map for easy checking
+var thumbnailMIMETypes = map[string]struct{}{
+	"image/gif":  struct{}{},
+	"image/jpeg": struct{}{},
+	"image/png":  struct{}{},
+	"image/webp": struct{}{},
+}
+
+// AcceptedMIMEType checks if a MIME type is suitable for thumbnailing.
+func AcceptedMIMEType(mime string) bool {
+	_, ok := thumbnailMIMETypes[mime]
+	return ok
+}
+
+// Pool operations for reusing *lillput.ImageOps objects.
+var imageOpsPool = &sync.Pool{
+	New: func() interface{} {
+		return lilliput.NewImageOps(maxInputResolution)
+	},
+}
+
+func getImageOps() *lilliput.ImageOps {
+	return imageOpsPool.Get().(*lilliput.ImageOps)
+}
+func returnImageOps(imageOps *lilliput.ImageOps) {
+	imageOpsPool.Put(imageOps)
+}
+
+// Transform takes image data and resizes it to fit the maxOutputResolution above.
+func Transform(data io.Reader) (*bytes.Buffer, error) {
+	b, err := ioutil.ReadAll(data)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read in image data")
+	}
+
+	// Create decoder and parse header
+	decoder, err := lilliput.NewDecoder(b)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode image data")
+	}
+	header, err := decoder.Header()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to parse image header")
+	}
+
+	// Check if the image is within the input limit
+	if header.Width() > maxInputResolution || header.Height() > maxInputResolution {
+		return nil, InputTooLarge
+	}
+
+	// Determine output resolution
+	width := -1
+	height := -1
+	if header.Width() == header.Height() {
+		width = maxOutputResolution
+		height = maxOutputResolution
+	} else if header.Width() > header.Height() {
+		width = 200
+		scale := header.Width() / maxOutputResolution
+		height = header.Height() / scale
+	} else {
+		height = 200
+		scale := header.Height() / maxOutputResolution
+		width = header.Width() / scale
+	}
+
+	// Prepare to resize image
+	ops := getImageOps()
+	outputImage := make([]byte, outputBufferSize)
+
+	// Resizing options
+	opts := &lilliput.ImageOptions{
+		FileType:             outputType,
+		Width:                width,
+		Height:               height,
+		ResizeMethod:         lilliput.ImageOpsFit,
+		NormalizeOrientation: true,
+		EncodeOptions:        encodeOptions,
+	}
+	if header.Width() <= maxOutputResolution && header.Height() <= maxOutputResolution {
+		opts.Width = header.Width()
+		opts.Height = header.Height()
+		opts.ResizeMethod = lilliput.ImageOpsNoResize
+	}
+
+	// Transform the image
+	outputImage, err = ops.Transform(decoder, opts, outputImage)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to transcode image")
+	}
+	returnImageOps(ops)
+	return bytes.NewBuffer(outputImage), err
+}
diff --git a/main.go b/main.go
index 8fa0565e29128c79292657825df83f9094a3e2fa..9c9ce8e8af14ca8c89fb6e1ebc65ade65023c056 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,11 @@
 package main
 
 import (
+	"bytes"
 	"database/sql"
 	"fmt"
 	"html/template"
+	"io"
 	"net"
 	"os"
 	"path/filepath"
@@ -13,23 +15,33 @@ import (
 
 	"owo.codes/whats-this/cdn-origin/lib/db"
 	"owo.codes/whats-this/cdn-origin/lib/metrics"
+	"owo.codes/whats-this/cdn-origin/lib/thumbnailer"
 
+	_ "github.com/lib/pq"
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/log"
-	_ "github.com/lib/pq"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 	"github.com/valyala/fasthttp"
 )
 
-// version is the current version of cdn-origin.
 // Build config
 const (
 	configLocationUnix = "/etc/whats-this/cdn-origin/config.toml"
-	shutdownTimeout = 10 *time.Second
-	version = "0.5.0"
+	version            = "0.7.0"
 )
 
+// readCloserBuffer is a *bytes.Buffer that implements io.ReadCloser.
+type readCloserBuffer struct {
+	*bytes.Buffer
+}
+
+func (b *readCloserBuffer) Close() error {
+	return nil
+}
+
+var _ io.ReadCloser = &readCloserBuffer{}
+
 // redirectHTML is the html/template template for generating redirect HTML.
 const redirectHTML = `<html><head><meta charset="UTF-8" /><meta http-equiv=refresh content="0; url={{.}}" /><script type="text/javascript">window.location.href="{{.}}"</script><title>Redirect</title></head><body><p>If you are not redirected automatically, click <a href="{{.}}">here</a> to go to the destination.</p></body></html>`
 
@@ -131,6 +143,9 @@ func init() {
 	if viper.GetString("files.storageLocation") == "" {
 		log.Fatal().Msg("Configuration: files.storageLocation is required")
 	}
+	if viper.GetBool("thumbnails.enable") && viper.GetBool("thumbnails.cacheEnable") && viper.GetString("thumbnails.cacheLocation") == "" {
+		log.Fatal().Msg("thumbnails.cacheLocation is required when thumbnails and thumbnails cache is enabled")
+	}
 
 	// Parse redirect templates
 	redirectHTMLTemplate, err = template.New("redirectHTML").Parse(redirectHTML)
@@ -144,6 +159,7 @@ func init() {
 }
 
 var collector *metrics.Collector
+var thumbnailCache *thumbnailer.ThumbnailCache
 
 func main() {
 	// Connect to PostgreSQL database
@@ -177,6 +193,11 @@ func main() {
 		}
 	}
 
+	// Setup thumbnail cache
+	if viper.GetBool("thumbnails.enable") && viper.GetBool("thumbnails.cacheEnable") {
+		thumbnailCache = thumbnailer.NewThumbnailCache(viper.GetString("thumbnails.cacheLocation"))
+	}
+
 	// Launch server
 	h := requestHandler
 	if viper.GetBool("http.compressResponse") {
@@ -252,6 +273,8 @@ func recordMetrics(ctx *fasthttp.RequestCtx) {
 }
 
 func requestHandler(ctx *fasthttp.RequestCtx) {
+	defer recordMetrics(ctx)
+
 	// Fetch object from database
 	key := string(ctx.Path()[1:])
 	object, err := db.SelectObjectByBucketKey(viper.GetString("database.objectBucket"), key)
@@ -260,14 +283,10 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 		ctx.SetStatusCode(fasthttp.StatusNotFound)
 		ctx.SetContentType("text/plain; charset=utf8")
 		fmt.Fprintf(ctx, "404 Not Found: %s", ctx.Path())
-		recordMetrics(ctx)
 		return
 	case err != nil:
 		log.Error().Err(err).Msg("failed to run SELECT query on database")
-		ctx.SetStatusCode(fasthttp.StatusInternalServerError)
-		ctx.SetContentType("text/plain; charset=utf8")
-		fmt.Fprint(ctx, "500 Internal Server Error")
-		recordMetrics(ctx)
+		internalServerError(ctx)
 		return
 	}
 
@@ -275,6 +294,98 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 	case 0: // file
 		ctx.SetUserValue("object_type", "file")
 
+		// Thumbnails
+		if viper.GetBool("thumbnails.enable") && ctx.QueryArgs().Has("thumbnail") {
+			thumbnailKey := *object.MD5Hash
+			if !thumbnailer.AcceptedMIMEType(*object.ContentType) || thumbnailKey == "" {
+				ctx.SetStatusCode(fasthttp.StatusNotFound)
+				ctx.SetContentType("text/plain; charset=utf8")
+				fmt.Fprintf(ctx, "404 Not Found: %s?thumbnail (cannot generate thumbnail)", ctx.Path())
+				return
+			}
+
+			// Get thumbnail
+			var thumb io.ReadCloser
+			if viper.GetBool("thumbnails.cacheEnable") {
+				thumb, err = thumbnailCache.GetThumbnail(thumbnailKey)
+				if thumb != nil {
+					defer thumb.Close()
+				}
+				if err == thumbnailer.NoCachedCopy {
+					fPath := filepath.Join(viper.GetString("files.storageLocation"), key)
+					file, err := os.Open(fPath)
+					if file != nil {
+						defer file.Close()
+					}
+					if err != nil {
+						log.Warn().Err(err).Msg("failed to open original file to generate thumbnail")
+						internalServerError(ctx)
+						return
+					}
+					err = thumbnailCache.Transform(thumbnailKey, file)
+					if err == thumbnailer.InputTooLarge {
+						ctx.SetStatusCode(fasthttp.StatusNotFound)
+						ctx.SetContentType("text/plain; charset=utf8")
+						fmt.Fprintf(ctx, "404 Not Found: %s?thumbnail (cannot generate thumbnail)", ctx.Path())
+						return
+					} else if err != nil {
+						log.Warn().Err(err).Msg("failed to generate new thumbnail")
+						internalServerError(ctx)
+						return
+					}
+					thumb, err = thumbnailCache.GetThumbnail(thumbnailKey)
+					if thumb != nil {
+						defer thumb.Close()
+					}
+					if err != nil {
+						log.Warn().Err(err).Msg("failed to get thumbnail from cache")
+						internalServerError(ctx)
+						return
+					}
+				} else if err != nil {
+					log.Warn().Err(err).Msg("failed to get thumbnail from cache")
+					internalServerError(ctx)
+					return
+				}
+			} else {
+				fPath := filepath.Join(viper.GetString("files.storageLocation"), key)
+				file, err := os.Open(fPath)
+				if file != nil {
+					defer file.Close()
+				}
+				if err != nil {
+					log.Warn().Err(err).Msg("failed to open original file to generate thumbnail")
+					internalServerError(ctx)
+					return
+				}
+				thumbR, err := thumbnailer.Transform(file)
+				if err == thumbnailer.InputTooLarge {
+					ctx.SetStatusCode(fasthttp.StatusNotFound)
+					ctx.SetContentType("text/plain; charset=utf8")
+					fmt.Fprintf(ctx, "404 Not Found: %s?thumbnail (cannot generate thumbnail)", ctx.Path())
+					return
+				} else if err != nil {
+					log.Warn().Err(err).Msg("failed to generate new thumbnail")
+					internalServerError(ctx)
+					return
+				}
+				// Turn the *bytes.Buffer from thumbnailer.Transform into a fake io.ReadCloser.
+				thumb = &readCloserBuffer{thumbR}
+			}
+
+			// Send response
+			ctx.SetStatusCode(fasthttp.StatusOK)
+			ctx.SetContentType("image/jpeg")
+			ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`filename="%s.thumbnail.jpeg"`, key))
+			_, err = io.Copy(ctx, thumb)
+			if err != nil {
+				log.Warn().Err(err).Msg("failed to send thumbnail response")
+				ctx.Response.Header.Del("Content-Disposition")
+				internalServerError(ctx)
+			}
+			return
+		}
+
 		// Serve file to client
 		fPath := filepath.Join(viper.GetString("files.storageLocation"), key)
 		ctx.SetStatusCode(fasthttp.StatusOK)
@@ -284,17 +395,13 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 			ctx.SetContentType("application/octet-stream")
 		}
 		fasthttp.ServeFileUncompressed(ctx, fPath)
-		recordMetrics(ctx)
 
 	case 1: // redirect
 		ctx.SetUserValue("object_type", "redirect")
 
 		if object.DestURL == nil {
 			log.Warn().Str("key", key).Msg("encountered redirect object with NULL dest_url")
-			ctx.SetStatusCode(fasthttp.StatusInternalServerError)
-			ctx.SetContentType("text/plain; charset=utf8")
-			fmt.Fprint(ctx, "500 Internal Server Error")
-			recordMetrics(ctx)
+			internalServerError(ctx)
 			return
 		}
 
@@ -309,11 +416,10 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 		if err != nil {
 			log.Warn().Err(err).
 				Str("dest_url", *object.DestURL).
-				Bool("preview", ctx.QueryArgs().
-				Has("preview")).Msg("failed to generate HTML redirect page to send to client")
+				Bool("preview", ctx.QueryArgs().Has("preview")).
+				Msg("failed to generate HTML redirect page to send to client")
 			ctx.SetContentType("text/plain; charset=utf8")
 			fmt.Fprintf(ctx, "Failed to generate HTML redirect page, destination URL: %s", *object.DestURL)
-			recordMetrics(ctx)
 			return
 		}
 
@@ -324,7 +430,6 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 		} else {
 			ctx.SetStatusCode(fasthttp.StatusOK)
 		}
-		recordMetrics(ctx)
 
 	case 2: // tombstone
 		ctx.SetUserValue("object_type", "tombstone")
@@ -337,6 +442,12 @@ func requestHandler(ctx *fasthttp.RequestCtx) {
 			reason = *object.DeleteReason
 		}
 		fmt.Fprintf(ctx, "410 Gone: %s\n\nReason: %s", ctx.Path(), reason)
-		recordMetrics(ctx)
 	}
 }
+
+// internalServerError returns a 500 Internal Server Response.
+func internalServerError(ctx *fasthttp.RequestCtx) {
+	ctx.SetStatusCode(fasthttp.StatusInternalServerError)
+	ctx.SetContentType("text/plain; charset=utf8")
+	fmt.Fprint(ctx, "500 Internal Server Error")
+}