caddyhttp: ResponseRecorder type for middlewares to buffer responses
Unfortunately, templates and markdown require buffering the full response before it can be processed and written to the clientpull/2663/head
parent
269b1e9aa3
commit
1c443beb9c
|
@ -1,8 +1,10 @@
|
||||||
package markdown
|
package markdown
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/russross/blackfriday.v2"
|
"gopkg.in/russross/blackfriday.v2"
|
||||||
|
|
||||||
|
@ -22,31 +24,35 @@ type Markdown struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
mrw := &markdownResponseWriter{
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
|
buf.Reset()
|
||||||
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
|
rr := caddyhttp.NewResponseRecorder(w, buf)
|
||||||
|
|
||||||
|
err := next.ServeHTTP(rr, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return next.ServeHTTP(mrw, r)
|
|
||||||
|
output := blackfriday.Run(buf.Bytes())
|
||||||
|
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(output)))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
|
||||||
|
w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
|
||||||
|
w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
|
||||||
|
|
||||||
|
w.WriteHeader(rr.Status())
|
||||||
|
w.Write(output)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type markdownResponseWriter struct {
|
var bufPool = sync.Pool{
|
||||||
*caddyhttp.ResponseWriterWrapper
|
New: func() interface{} {
|
||||||
statusCode int
|
return new(bytes.Buffer)
|
||||||
wroteHeader bool
|
},
|
||||||
}
|
|
||||||
|
|
||||||
func (mrw *markdownResponseWriter) WriteHeader(code int) {
|
|
||||||
mrw.statusCode = code
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mrw *markdownResponseWriter) Write(d []byte) (int, error) {
|
|
||||||
output := blackfriday.Run(d)
|
|
||||||
if !mrw.wroteHeader {
|
|
||||||
mrw.Header().Set("Content-Length", strconv.Itoa(len(output)))
|
|
||||||
mrw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
mrw.WriteHeader(mrw.statusCode)
|
|
||||||
mrw.wroteHeader = true
|
|
||||||
}
|
|
||||||
return mrw.ResponseWriter.Write(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
|
|
|
@ -2,6 +2,7 @@ package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -58,5 +59,75 @@ type HTTPInterfaces interface {
|
||||||
// ResponseWriter does not implement the required method.
|
// ResponseWriter does not implement the required method.
|
||||||
var ErrNotImplemented = fmt.Errorf("method not implemented")
|
var ErrNotImplemented = fmt.Errorf("method not implemented")
|
||||||
|
|
||||||
|
type responseRecorder struct {
|
||||||
|
*ResponseWriterWrapper
|
||||||
|
wroteHeader bool
|
||||||
|
statusCode int
|
||||||
|
buf *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResponseRecorder returns a new ResponseRecorder that can be
|
||||||
|
// used instead of a real http.ResponseWriter. The recorder is useful
|
||||||
|
// for middlewares which need to buffer a responder's response and
|
||||||
|
// process it in its entirety before actually allowing the response to
|
||||||
|
// be written. Of course, this has a performance overhead, but
|
||||||
|
// sometimes there is no way to avoid buffering the whole response.
|
||||||
|
// Still, if at all practical, middlewares should strive to stream
|
||||||
|
// responses by wrapping Write and WriteHeader methods instead of
|
||||||
|
// buffering whole response bodies.
|
||||||
|
//
|
||||||
|
// Before calling this function in a middleware handler, make a
|
||||||
|
// new buffer or obtain one from a pool (use the sync.Pool) type.
|
||||||
|
// Using a pool is generally recommended for performance gains;
|
||||||
|
// do profiling to ensure this is the case. If using a pool, be
|
||||||
|
// sure to reset the buffer before using it.
|
||||||
|
//
|
||||||
|
// The returned recorder can be used in place of w when calling
|
||||||
|
// the next handler in the chain. When that handler returns, you
|
||||||
|
// can read the status code from the recorder's Status() method.
|
||||||
|
// The response body fills buf, and the headers are available in
|
||||||
|
// w.Header().
|
||||||
|
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer) ResponseRecorder {
|
||||||
|
return &responseRecorder{
|
||||||
|
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
|
||||||
|
buf: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *responseRecorder) WriteHeader(statusCode int) {
|
||||||
|
if rr.wroteHeader {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rr.statusCode = statusCode
|
||||||
|
rr.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *responseRecorder) Write(data []byte) (int, error) {
|
||||||
|
rr.WriteHeader(http.StatusOK)
|
||||||
|
return rr.buf.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns the status code that was written, if any.
|
||||||
|
func (rr *responseRecorder) Status() int {
|
||||||
|
return rr.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer returns the body buffer that rr was created with.
|
||||||
|
// You should still have your original pointer, though.
|
||||||
|
func (rr *responseRecorder) Buffer() *bytes.Buffer {
|
||||||
|
return rr.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseRecorder is a http.ResponseWriter that records
|
||||||
|
// responses instead of writing them to the client.
|
||||||
|
type ResponseRecorder interface {
|
||||||
|
HTTPInterfaces
|
||||||
|
Status() int
|
||||||
|
Buffer() *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
|
var (
|
||||||
|
_ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
|
||||||
|
_ ResponseRecorder = (*responseRecorder)(nil)
|
||||||
|
)
|
||||||
|
|
|
@ -37,35 +37,31 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
wb := &responseBuffer{
|
rr := caddyhttp.NewResponseRecorder(w, buf)
|
||||||
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
|
|
||||||
buf: buf,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := next.ServeHTTP(wb, r)
|
err := next.ServeHTTP(rr, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.executeTemplate(wb, r)
|
err = t.executeTemplate(rr, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(wb.buf.Len()))
|
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||||
w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
|
w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
|
||||||
w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
|
w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
|
||||||
w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
|
w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
|
||||||
|
|
||||||
w.WriteHeader(wb.statusCode)
|
w.WriteHeader(rr.Status())
|
||||||
io.Copy(w, wb.buf)
|
io.Copy(w, buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeTemplate executes the template contianed
|
// executeTemplate executes the template contained in wb.buf and replaces it with the results.
|
||||||
// in wb.buf and replaces it with the results.
|
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
|
||||||
func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
|
|
||||||
var fs http.FileSystem
|
var fs http.FileSystem
|
||||||
if t.FileRoot != "" {
|
if t.FileRoot != "" {
|
||||||
fs = http.Dir(t.FileRoot)
|
fs = http.Dir(t.FileRoot)
|
||||||
|
@ -74,11 +70,11 @@ func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
|
||||||
ctx := &templateContext{
|
ctx := &templateContext{
|
||||||
Root: fs,
|
Root: fs,
|
||||||
Req: r,
|
Req: r,
|
||||||
RespHeader: tplWrappedHeader{wb.Header()},
|
RespHeader: tplWrappedHeader{rr.Header()},
|
||||||
config: t,
|
config: t,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ctx.executeTemplateInBuffer(r.URL.Path, wb.buf)
|
err := ctx.executeTemplateInBuffer(r.URL.Path, rr.Buffer())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
@ -86,29 +82,8 @@ func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseBuffer buffers the response so that it can be
|
// virtualResponseWriter is used in virtualized HTTP requests
|
||||||
// executed as a template.
|
// that templates may execute.
|
||||||
type responseBuffer struct {
|
|
||||||
*caddyhttp.ResponseWriterWrapper
|
|
||||||
wroteHeader bool
|
|
||||||
statusCode int
|
|
||||||
buf *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rb *responseBuffer) WriteHeader(statusCode int) {
|
|
||||||
if rb.wroteHeader {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rb.statusCode = statusCode
|
|
||||||
rb.wroteHeader = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rb *responseBuffer) Write(data []byte) (int, error) {
|
|
||||||
rb.WriteHeader(http.StatusOK)
|
|
||||||
return rb.buf.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// virtualResponseWriter is used in virtualized HTTP requests.
|
|
||||||
type virtualResponseWriter struct {
|
type virtualResponseWriter struct {
|
||||||
status int
|
status int
|
||||||
header http.Header
|
header http.Header
|
||||||
|
@ -131,5 +106,4 @@ func (vrw *virtualResponseWriter) Write(data []byte) (int, error) {
|
||||||
var (
|
var (
|
||||||
_ caddy.Validator = (*Templates)(nil)
|
_ caddy.Validator = (*Templates)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Templates)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Templates)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*responseBuffer)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue