@marcel91200 / caddy Mirror · read-only / commits / 1c443beb
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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

Matthew Holt committed Jun 20, 2019 at 21:49 UTC 1c443beb9c87b42d1d018f72e36ac9b15fdfccdc
3 files changed +110 -59
modules/caddyhttp/markdown/markdown.go
+26 -20
@@ -1,8 +1,10 @@
1 package markdown
2
3 import (
4 + "bytes"
5 "net/http"
6 "strconv"
7 + "sync"
8
9 "gopkg.in/russross/blackfriday.v2"
10
@@ -22,31 +24,35 @@ type Markdown struct {
24 }
25
26 func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
25 - mrw := &markdownResponseWriter{
26 - ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
27 + buf := bufPool.Get().(*bytes.Buffer)
28 + buf.Reset()
29 + defer bufPool.Put(buf)
30 +
31 + rr := caddyhttp.NewResponseRecorder(w, buf)
32 +
33 + err := next.ServeHTTP(rr, r)
34 + if err != nil {
35 + return err
36 }
28 - return next.ServeHTTP(mrw, r)
29 -}
37
31 -type markdownResponseWriter struct {
32 - *caddyhttp.ResponseWriterWrapper
33 - statusCode int
34 - wroteHeader bool
35 -}
38 + output := blackfriday.Run(buf.Bytes())
39
37 -func (mrw *markdownResponseWriter) WriteHeader(code int) {
38 - mrw.statusCode = code
40 + w.Header().Set("Content-Length", strconv.Itoa(len(output)))
41 + w.Header().Set("Content-Type", "text/html; charset=utf-8")
42 + w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
43 + w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
44 + w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
45 +
46 + w.WriteHeader(rr.Status())
47 + w.Write(output)
48 +
49 + return nil
50 }
51
41 -func (mrw *markdownResponseWriter) Write(d []byte) (int, error) {
42 - output := blackfriday.Run(d)
43 - if !mrw.wroteHeader {
44 - mrw.Header().Set("Content-Length", strconv.Itoa(len(output)))
45 - mrw.Header().Set("Content-Type", "text/html; charset=utf-8")
46 - mrw.WriteHeader(mrw.statusCode)
47 - mrw.wroteHeader = true
48 - }
49 - return mrw.ResponseWriter.Write(output)
52 +var bufPool = sync.Pool{
53 + New: func() interface{} {
54 + return new(bytes.Buffer)
55 + },
56 }
57
58 // Interface guard
modules/caddyhttp/responsewriter.go
+72 -1
@@ -2,6 +2,7 @@ package caddyhttp
2
3 import (
4 "bufio"
5 + "bytes"
6 "fmt"
7 "net"
8 "net/http"
@@ -58,5 +59,75 @@ type HTTPInterfaces interface {
59 // ResponseWriter does not implement the required method.
60 var ErrNotImplemented = fmt.Errorf("method not implemented")
61
62 +type responseRecorder struct {
63 + *ResponseWriterWrapper
64 + wroteHeader bool
65 + statusCode int
66 + buf *bytes.Buffer
67 +}
68 +
69 +// NewResponseRecorder returns a new ResponseRecorder that can be
70 +// used instead of a real http.ResponseWriter. The recorder is useful
71 +// for middlewares which need to buffer a responder's response and
72 +// process it in its entirety before actually allowing the response to
73 +// be written. Of course, this has a performance overhead, but
74 +// sometimes there is no way to avoid buffering the whole response.
75 +// Still, if at all practical, middlewares should strive to stream
76 +// responses by wrapping Write and WriteHeader methods instead of
77 +// buffering whole response bodies.
78 +//
79 +// Before calling this function in a middleware handler, make a
80 +// new buffer or obtain one from a pool (use the sync.Pool) type.
81 +// Using a pool is generally recommended for performance gains;
82 +// do profiling to ensure this is the case. If using a pool, be
83 +// sure to reset the buffer before using it.
84 +//
85 +// The returned recorder can be used in place of w when calling
86 +// the next handler in the chain. When that handler returns, you
87 +// can read the status code from the recorder's Status() method.
88 +// The response body fills buf, and the headers are available in
89 +// w.Header().
90 +func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer) ResponseRecorder {
91 + return &responseRecorder{
92 + ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
93 + buf: buf,
94 + }
95 +}
96 +
97 +func (rr *responseRecorder) WriteHeader(statusCode int) {
98 + if rr.wroteHeader {
99 + return
100 + }
101 + rr.statusCode = statusCode
102 + rr.wroteHeader = true
103 +}
104 +
105 +func (rr *responseRecorder) Write(data []byte) (int, error) {
106 + rr.WriteHeader(http.StatusOK)
107 + return rr.buf.Write(data)
108 +}
109 +
110 +// Status returns the status code that was written, if any.
111 +func (rr *responseRecorder) Status() int {
112 + return rr.statusCode
113 +}
114 +
115 +// Buffer returns the body buffer that rr was created with.
116 +// You should still have your original pointer, though.
117 +func (rr *responseRecorder) Buffer() *bytes.Buffer {
118 + return rr.buf
119 +}
120 +
121 +// ResponseRecorder is a http.ResponseWriter that records
122 +// responses instead of writing them to the client.
123 +type ResponseRecorder interface {
124 + HTTPInterfaces
125 + Status() int
126 + Buffer() *bytes.Buffer
127 +}
128 +
129 // Interface guards
62 -var _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
130 +var (
131 + _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
132 + _ ResponseRecorder = (*responseRecorder)(nil)
133 +)
modules/caddyhttp/templates/templates.go
+12 -38
@@ -37,35 +37,31 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
37 buf.Reset()
38 defer bufPool.Put(buf)
39
40 - wb := &responseBuffer{
41 - ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
42 - buf: buf,
43 - }
40 + rr := caddyhttp.NewResponseRecorder(w, buf)
41
45 - err := next.ServeHTTP(wb, r)
42 + err := next.ServeHTTP(rr, r)
43 if err != nil {
44 return err
45 }
46
50 - err = t.executeTemplate(wb, r)
47 + err = t.executeTemplate(rr, r)
48 if err != nil {
49 return err
50 }
51
55 - w.Header().Set("Content-Length", strconv.Itoa(wb.buf.Len()))
52 + w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
53 w.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content
54 w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
55 w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
56
60 - w.WriteHeader(wb.statusCode)
61 - io.Copy(w, wb.buf)
57 + w.WriteHeader(rr.Status())
58 + io.Copy(w, buf)
59
60 return nil
61 }
62
66 -// executeTemplate executes the template contianed
67 -// in wb.buf and replaces it with the results.
68 -func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
63 +// executeTemplate executes the template contained in wb.buf and replaces it with the results.
64 +func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
65 var fs http.FileSystem
66 if t.FileRoot != "" {
67 fs = http.Dir(t.FileRoot)
@@ -74,11 +70,11 @@ func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
70 ctx := &templateContext{
71 Root: fs,
72 Req: r,
77 - RespHeader: tplWrappedHeader{wb.Header()},
73 + RespHeader: tplWrappedHeader{rr.Header()},
74 config: t,
75 }
76
81 - err := ctx.executeTemplateInBuffer(r.URL.Path, wb.buf)
77 + err := ctx.executeTemplateInBuffer(r.URL.Path, rr.Buffer())
78 if err != nil {
79 return caddyhttp.Error(http.StatusInternalServerError, err)
80 }
@@ -86,29 +82,8 @@ func (t *Templates) executeTemplate(wb *responseBuffer, r *http.Request) error {
82 return nil
83 }
84
89 -// responseBuffer buffers the response so that it can be
90 -// executed as a template.
91 -type responseBuffer struct {
92 - *caddyhttp.ResponseWriterWrapper
93 - wroteHeader bool
94 - statusCode int
95 - buf *bytes.Buffer
96 -}
97 -
98 -func (rb *responseBuffer) WriteHeader(statusCode int) {
99 - if rb.wroteHeader {
100 - return
101 - }
102 - rb.statusCode = statusCode
103 - rb.wroteHeader = true
104 -}
105 -
106 -func (rb *responseBuffer) Write(data []byte) (int, error) {
107 - rb.WriteHeader(http.StatusOK)
108 - return rb.buf.Write(data)
109 -}
110 -
111 -// virtualResponseWriter is used in virtualized HTTP requests.
85 +// virtualResponseWriter is used in virtualized HTTP requests
86 +// that templates may execute.
87 type virtualResponseWriter struct {
88 status int
89 header http.Header
@@ -131,5 +106,4 @@ func (vrw *virtualResponseWriter) Write(data []byte) (int, error) {
106 var (
107 _ caddy.Validator = (*Templates)(nil)
108 _ caddyhttp.MiddlewareHandler = (*Templates)(nil)
134 - _ caddyhttp.HTTPInterfaces = (*responseBuffer)(nil)
109 )