mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-25 09:40:13 +08:00
Various bug fixes and minor improvements
- Fix static responder so it doesn't replace its own headers config, and instead replaces the actual response header values - caddyhttp.ResponseRecorder type optionally buffers response - Add interface guards to ensure regexp matchers get provisioned - Use default HTTP port if one is not explicitly set - Encode middleware writes status code 200 if not written upstream - Templates and markdown only try to execute on text responses - Static file server sets Content-Type based on file extension only (this whole thing -- MIME sniffing, etc -- needs more configurability)
This commit is contained in:
parent
81a9e125b5
commit
d49f762f6d
|
@ -265,7 +265,12 @@ func (app *App) automaticHTTPS() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
|
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
|
||||||
}
|
}
|
||||||
httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(app.HTTPPort))
|
|
||||||
|
httpPort := app.HTTPPort
|
||||||
|
if httpPort == 0 {
|
||||||
|
httpPort = DefaultHTTPPort
|
||||||
|
}
|
||||||
|
httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(httpPort))
|
||||||
lnAddrMap[httpRedirLnAddr] = struct{}{}
|
lnAddrMap[httpRedirLnAddr] = struct{}{}
|
||||||
|
|
||||||
if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
|
if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
|
||||||
|
|
|
@ -157,7 +157,11 @@ func (rw *responseWriter) init() {
|
||||||
rw.Header().Set("Content-Encoding", rw.encodingName)
|
rw.Header().Set("Content-Encoding", rw.encodingName)
|
||||||
}
|
}
|
||||||
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
|
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
|
||||||
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
status := rw.statusCode
|
||||||
|
if status == 0 {
|
||||||
|
status = http.StatusOK
|
||||||
|
}
|
||||||
|
rw.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close writes any remaining buffered response and
|
// Close writes any remaining buffered response and
|
||||||
|
|
|
@ -66,6 +66,7 @@ func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *ht
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteTo(w)
|
buf.WriteTo(w)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -185,14 +186,21 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error
|
||||||
|
|
||||||
// TODO: Etag
|
// TODO: Etag
|
||||||
|
|
||||||
// do not allow Go to sniff the content-type
|
|
||||||
if w.Header().Get("Content-Type") == "" {
|
if w.Header().Get("Content-Type") == "" {
|
||||||
w.Header()["Content-Type"] = nil
|
mtyp := mime.TypeByExtension(filepath.Ext(filename))
|
||||||
|
if mtyp == "" {
|
||||||
|
// do not allow Go to sniff the content-type; see
|
||||||
|
// https://www.youtube.com/watch?v=8t8JYpt0egE
|
||||||
|
// TODO: Consider writing a default mime type of application/octet-stream - this is secure but violates spec
|
||||||
|
w.Header()["Content-Type"] = nil
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", mtyp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let the standard library do what it does best; note, however,
|
// let the standard library do what it does best; note, however,
|
||||||
// that errors generated by ServeContent are written immediately
|
// that errors generated by ServeContent are written immediately
|
||||||
// to the response, so we cannot handle them (but errors here
|
// to the response, so we cannot handle them (but errors there
|
||||||
// are rare)
|
// are rare)
|
||||||
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
||||||
|
|
||||||
|
|
|
@ -40,20 +40,25 @@ type RespHeaderOps struct {
|
||||||
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
apply(h.Request, r.Header, repl)
|
apply(h.Request, r.Header, repl)
|
||||||
if h.Response.Deferred || h.Response.Require != nil {
|
if h.Response != nil {
|
||||||
w = &responseWriterWrapper{
|
if h.Response.Deferred || h.Response.Require != nil {
|
||||||
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
|
w = &responseWriterWrapper{
|
||||||
replacer: repl,
|
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
|
||||||
require: h.Response.Require,
|
replacer: repl,
|
||||||
headerOps: h.Response.HeaderOps,
|
require: h.Response.Require,
|
||||||
|
headerOps: h.Response.HeaderOps,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apply(h.Response.HeaderOps, w.Header(), repl)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
apply(h.Response.HeaderOps, w.Header(), repl)
|
|
||||||
}
|
}
|
||||||
return next.ServeHTTP(w, r)
|
return next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(ops *HeaderOps, hdr http.Header, repl caddy.Replacer) {
|
func apply(ops *HeaderOps, hdr http.Header, repl caddy.Replacer) {
|
||||||
|
if ops == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
for fieldName, vals := range ops.Add {
|
for fieldName, vals := range ops.Add {
|
||||||
fieldName = repl.ReplaceAll(fieldName, "")
|
fieldName = repl.ReplaceAll(fieldName, "")
|
||||||
for _, v := range vals {
|
for _, v := range vals {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/russross/blackfriday.v2"
|
"gopkg.in/russross/blackfriday.v2"
|
||||||
|
@ -28,12 +29,19 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
rr := caddyhttp.NewResponseRecorder(w, buf)
|
shouldBuf := func(status int) bool {
|
||||||
|
return strings.HasPrefix(w.Header().Get("Content-Type"), "text/")
|
||||||
|
}
|
||||||
|
|
||||||
err := next.ServeHTTP(rr, r)
|
rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf)
|
||||||
|
|
||||||
|
err := next.ServeHTTP(rec, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !rec.Buffered() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
output := blackfriday.Run(buf.Bytes())
|
output := blackfriday.Run(buf.Bytes())
|
||||||
|
|
||||||
|
@ -43,7 +51,7 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
||||||
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(rr.Status())
|
w.WriteHeader(rec.Status())
|
||||||
w.Write(output)
|
w.Write(output)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -226,9 +226,9 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision compiles m's regular expressions.
|
// Provision compiles m's regular expressions.
|
||||||
func (m MatchHeaderRE) Provision() error {
|
func (m MatchHeaderRE) Provision(ctx caddy.Context) error {
|
||||||
for _, rm := range m {
|
for _, rm := range m {
|
||||||
err := rm.Provision()
|
err := rm.Provision(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -371,7 +371,7 @@ type MatchRegexp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision compiles the regular expression.
|
// Provision compiles the regular expression.
|
||||||
func (mre *MatchRegexp) Provision() error {
|
func (mre *MatchRegexp) Provision(caddy.Context) error {
|
||||||
re, err := regexp.Compile(mre.Pattern)
|
re, err := regexp.Compile(mre.Pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
|
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
|
||||||
|
@ -479,14 +479,17 @@ var (
|
||||||
_ RequestMatcher = (*MatchHost)(nil)
|
_ RequestMatcher = (*MatchHost)(nil)
|
||||||
_ RequestMatcher = (*MatchPath)(nil)
|
_ RequestMatcher = (*MatchPath)(nil)
|
||||||
_ RequestMatcher = (*MatchPathRE)(nil)
|
_ RequestMatcher = (*MatchPathRE)(nil)
|
||||||
|
_ caddy.Provisioner = (*MatchPathRE)(nil)
|
||||||
_ RequestMatcher = (*MatchMethod)(nil)
|
_ RequestMatcher = (*MatchMethod)(nil)
|
||||||
_ RequestMatcher = (*MatchQuery)(nil)
|
_ RequestMatcher = (*MatchQuery)(nil)
|
||||||
_ RequestMatcher = (*MatchHeader)(nil)
|
_ RequestMatcher = (*MatchHeader)(nil)
|
||||||
_ RequestMatcher = (*MatchHeaderRE)(nil)
|
_ RequestMatcher = (*MatchHeaderRE)(nil)
|
||||||
|
_ caddy.Provisioner = (*MatchHeaderRE)(nil)
|
||||||
_ RequestMatcher = (*MatchProtocol)(nil)
|
_ RequestMatcher = (*MatchProtocol)(nil)
|
||||||
_ RequestMatcher = (*MatchRemoteIP)(nil)
|
_ RequestMatcher = (*MatchRemoteIP)(nil)
|
||||||
_ caddy.Provisioner = (*MatchRemoteIP)(nil)
|
_ caddy.Provisioner = (*MatchRemoteIP)(nil)
|
||||||
_ RequestMatcher = (*MatchNegate)(nil)
|
_ RequestMatcher = (*MatchNegate)(nil)
|
||||||
_ caddy.Provisioner = (*MatchNegate)(nil)
|
_ caddy.Provisioner = (*MatchNegate)(nil)
|
||||||
_ RequestMatcher = (*MatchStarlarkExpr)(nil)
|
_ RequestMatcher = (*MatchStarlarkExpr)(nil)
|
||||||
|
_ caddy.Provisioner = (*MatchRegexp)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -219,7 +219,7 @@ func TestPathREMatcher(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
// compile the regexp and validate its name
|
// compile the regexp and validate its name
|
||||||
err := tc.match.Provision()
|
err := tc.match.Provision(caddy.Context{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
|
t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
|
||||||
continue
|
continue
|
||||||
|
@ -337,7 +337,7 @@ func TestHeaderREMatcher(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
// compile the regexp and validate its name
|
// compile the regexp and validate its name
|
||||||
err := tc.match.Provision()
|
err := tc.match.Provision(caddy.Context{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
|
t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -61,9 +61,11 @@ var ErrNotImplemented = fmt.Errorf("method not implemented")
|
||||||
|
|
||||||
type responseRecorder struct {
|
type responseRecorder struct {
|
||||||
*ResponseWriterWrapper
|
*ResponseWriterWrapper
|
||||||
wroteHeader bool
|
wroteHeader bool
|
||||||
statusCode int
|
statusCode int
|
||||||
buf *bytes.Buffer
|
buf *bytes.Buffer
|
||||||
|
shouldBuffer func(status int) bool
|
||||||
|
stream bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResponseRecorder returns a new ResponseRecorder that can be
|
// NewResponseRecorder returns a new ResponseRecorder that can be
|
||||||
|
@ -76,6 +78,16 @@ type responseRecorder struct {
|
||||||
// responses by wrapping Write and WriteHeader methods instead of
|
// responses by wrapping Write and WriteHeader methods instead of
|
||||||
// buffering whole response bodies.
|
// buffering whole response bodies.
|
||||||
//
|
//
|
||||||
|
// Recorders optionally buffer the response. When the headers are
|
||||||
|
// to be written, shouldBuffer will be called with the status
|
||||||
|
// code that is being written. The rest of the headers can be read
|
||||||
|
// from w.Header(). If shouldBuffer returns true, the response
|
||||||
|
// will be buffered. You can know the response was buffered if
|
||||||
|
// the Buffered() method returns true. If the response was not
|
||||||
|
// buffered, Buffered() will return false and that means the
|
||||||
|
// response bypassed the recorder and was written directly to the
|
||||||
|
// underlying writer.
|
||||||
|
//
|
||||||
// Before calling this function in a middleware handler, make a
|
// Before calling this function in a middleware handler, make a
|
||||||
// new buffer or obtain one from a pool (use the sync.Pool) type.
|
// new buffer or obtain one from a pool (use the sync.Pool) type.
|
||||||
// Using a pool is generally recommended for performance gains;
|
// Using a pool is generally recommended for performance gains;
|
||||||
|
@ -85,12 +97,13 @@ type responseRecorder struct {
|
||||||
// The returned recorder can be used in place of w when calling
|
// The returned recorder can be used in place of w when calling
|
||||||
// the next handler in the chain. When that handler returns, you
|
// the next handler in the chain. When that handler returns, you
|
||||||
// can read the status code from the recorder's Status() method.
|
// can read the status code from the recorder's Status() method.
|
||||||
// The response body fills buf, and the headers are available in
|
// The response body fills buf if it was buffered, and the headers
|
||||||
// w.Header().
|
// are available via w.Header().
|
||||||
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer) ResponseRecorder {
|
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer func(status int) bool) ResponseRecorder {
|
||||||
return &responseRecorder{
|
return &responseRecorder{
|
||||||
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
|
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
|
||||||
buf: buf,
|
buf: buf,
|
||||||
|
shouldBuffer: shouldBuffer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +113,22 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
|
||||||
}
|
}
|
||||||
rr.statusCode = statusCode
|
rr.statusCode = statusCode
|
||||||
rr.wroteHeader = true
|
rr.wroteHeader = true
|
||||||
|
|
||||||
|
// decide whether we should buffer the response
|
||||||
|
if rr.shouldBuffer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rr.stream = !rr.shouldBuffer(rr.statusCode)
|
||||||
|
if rr.stream {
|
||||||
|
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *responseRecorder) Write(data []byte) (int, error) {
|
func (rr *responseRecorder) Write(data []byte) (int, error) {
|
||||||
rr.WriteHeader(http.StatusOK)
|
rr.WriteHeader(http.StatusOK)
|
||||||
|
if rr.stream {
|
||||||
|
return rr.ResponseWriterWrapper.Write(data)
|
||||||
|
}
|
||||||
return rr.buf.Write(data)
|
return rr.buf.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,12 +143,18 @@ func (rr *responseRecorder) Buffer() *bytes.Buffer {
|
||||||
return rr.buf
|
return rr.buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffered returns whether rr has decided to buffer the response.
|
||||||
|
func (rr *responseRecorder) Buffered() bool {
|
||||||
|
return !rr.stream
|
||||||
|
}
|
||||||
|
|
||||||
// ResponseRecorder is a http.ResponseWriter that records
|
// ResponseRecorder is a http.ResponseWriter that records
|
||||||
// responses instead of writing them to the client.
|
// responses instead of writing them to the client.
|
||||||
type ResponseRecorder interface {
|
type ResponseRecorder interface {
|
||||||
HTTPInterfaces
|
HTTPInterfaces
|
||||||
Status() int
|
Status() int
|
||||||
Buffer() *bytes.Buffer
|
Buffer() *bytes.Buffer
|
||||||
|
Buffered() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
|
|
|
@ -72,11 +72,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
err := s.executeCompositeRoute(w, r, errStack)
|
err := s.executeCompositeRoute(w, r, errStack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: what should we do if the error handler has an error?
|
// TODO: what should we do if the error handler has an error?
|
||||||
log.Printf("[ERROR] handling error: %v", err)
|
log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: polish the default error handling
|
// TODO: polish the default error handling
|
||||||
log.Printf("[ERROR] Handler: %s", err)
|
log.Printf("[ERROR] [%s %s] %v", r.Method, r.RequestURI, err)
|
||||||
if handlerErr, ok := err.(HandlerError); ok {
|
if handlerErr, ok := err.(HandlerError); ok {
|
||||||
w.WriteHeader(handlerErr.StatusCode)
|
w.WriteHeader(handlerErr.StatusCode)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,10 +33,11 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
// set all headers
|
// set all headers
|
||||||
for field, vals := range s.Headers {
|
for field, vals := range s.Headers {
|
||||||
field = repl.ReplaceAll(field, "")
|
field = repl.ReplaceAll(field, "")
|
||||||
|
newVals := make([]string, len(vals))
|
||||||
for i := range vals {
|
for i := range vals {
|
||||||
vals[i] = repl.ReplaceAll(vals[i], "")
|
newVals[i] = repl.ReplaceAll(vals[i], "")
|
||||||
}
|
}
|
||||||
w.Header()[field] = vals
|
w.Header()[field] = newVals
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not allow Go to sniff the content-type
|
// do not allow Go to sniff the content-type
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy"
|
"github.com/caddyserver/caddy"
|
||||||
"github.com/caddyserver/caddy/modules/caddyhttp"
|
"github.com/caddyserver/caddy/modules/caddyhttp"
|
||||||
|
@ -37,14 +38,21 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
rr := caddyhttp.NewResponseRecorder(w, buf)
|
shouldBuf := func(status int) bool {
|
||||||
|
return strings.HasPrefix(w.Header().Get("Content-Type"), "text/")
|
||||||
|
}
|
||||||
|
|
||||||
err := next.ServeHTTP(rr, r)
|
rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf)
|
||||||
|
|
||||||
|
err := next.ServeHTTP(rec, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !rec.Buffered() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err = t.executeTemplate(rr, r)
|
err = t.executeTemplate(rec, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -54,7 +62,7 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||||
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(rr.Status())
|
w.WriteHeader(rec.Status())
|
||||||
io.Copy(w, buf)
|
io.Copy(w, buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in New Issue
Block a user