mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-19 09:32:46 +08:00
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 client
This commit is contained in:
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
Block a user