Skip to content

Commit

Permalink
Fix ResponseWriter to not minify if there is no supported^CequestURI …
Browse files Browse the repository at this point in the history
…extension or supported Content-Type header, fixes #660
  • Loading branch information
tdewolff committed Jan 25, 2024
1 parent 5eba9db commit f9aa0ad
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 36 deletions.
72 changes: 42 additions & 30 deletions minify.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var Warning = log.New(os.Stderr, "WARNING: ", 0)
var ErrNotExist = errors.New("minifier does not exist for mimetype")

// ErrClosedWriter is returned when writing to a closed writer.
var ErrClosedWriter = errors.New("write on closed writer")
var ErrClosedWriter = errors.New("write on closed writer") // TODO: DEPRECATED, remove

////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -253,50 +253,48 @@ func (m *M) Reader(mediatype string, r io.Reader) io.Reader {

// writer makes sure that errors from the minifier are passed down through Close (can be blocking).
type writer struct {
pw *io.PipeWriter
w io.WriteCloser
wg sync.WaitGroup
err error
closed bool
err error
}

// Write intercepts any writes to the writer.
func (w *writer) Write(b []byte) (int, error) {
if w.closed {
return 0, ErrClosedWriter
func (z *writer) Write(b []byte) (int, error) {
if z.err != nil {
return 0, z.err
}
n, err := w.pw.Write(b)
if w.err != nil {
err = w.err
}
return n, err
return z.w.Write(b)
}

// Close must be called when writing has finished. It returns the error from the minifier.
func (w *writer) Close() error {
if !w.closed {
w.pw.Close()
w.wg.Wait()
w.closed = true
func (z *writer) Close() error {
if z.closed {
return nil
}
z.closed = true
if err := z.w.Close(); z.err == nil {
z.err = err
}
return w.err
z.wg.Wait()
return z.err
}

// Writer wraps a Writer interface and minifies the stream.
// Errors from the minifier are returned by Close on the writer.
// The writer must be closed explicitly.
func (m *M) Writer(mediatype string, w io.Writer) io.WriteCloser {
pr, pw := io.Pipe()
mw := &writer{pw, sync.WaitGroup{}, nil, false}
mw.wg.Add(1)
z := &writer{pw, sync.WaitGroup{}, false, nil}
z.wg.Add(1)
go func() {
defer mw.wg.Done()

defer z.wg.Done()
defer pr.Close()
if err := m.Minify(mediatype, w, pr); err != nil {
mw.err = err
z.err = err
}
pr.Close()
}()
return mw
return z
}

// responseWriter wraps an http.ResponseWriter and makes sure that errors from the minifier are passed down through Close (can be blocking).
Expand All @@ -305,7 +303,7 @@ func (m *M) Writer(mediatype string, w io.Writer) io.WriteCloser {
type responseWriter struct {
http.ResponseWriter

writer *writer
z io.Writer
m *M
mediatype string
}
Expand All @@ -319,20 +317,34 @@ func (w *responseWriter) WriteHeader(status int) {
// Write intercepts any writes to the response writer.
// The first write will extract the Content-Type as the mediatype. Otherwise it falls back to the RequestURI extension.
func (w *responseWriter) Write(b []byte) (int, error) {
if w.writer == nil {
if w.z == nil {
// first write
if mediatype := w.ResponseWriter.Header().Get("Content-Type"); mediatype != "" {
w.mediatype = mediatype
}
w.writer = w.m.Writer(w.mediatype, w.ResponseWriter).(*writer)
if _, params, minifier := w.m.Match(w.mediatype); minifier != nil {
pr, pw := io.Pipe()
z := &writer{pw, sync.WaitGroup{}, false, nil}
z.wg.Add(1)
go func() {
defer z.wg.Done()
defer pr.Close()
if err := minifier(w.m, w.ResponseWriter, pr, params); err != nil {
z.err = err
}
}()
w.z = z
} else {
w.z = w.ResponseWriter
}
}
return w.writer.Write(b)
return w.z.Write(b)
}

// Close must be called when writing has finished. It returns the error from the minifier.
func (w *responseWriter) Close() error {
if w.writer != nil {
return w.writer.Close()
if closer, ok := w.z.(interface{ Close() error }); ok {
return closer.Close()
}
return nil
}
Expand Down
25 changes: 19 additions & 6 deletions minify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,28 +246,41 @@ func (w *testResponseWriter) Write(b []byte) (int, error) {
func TestResponseWriter(t *testing.T) {
m := New()
m.AddFunc("text/html", func(m *M, w io.Writer, r io.Reader, _ map[string]string) error {
_, err := io.Copy(w, r)
_, _ = io.ReadAll(r)
_, err := w.Write([]byte("minified"))
return err
})

// use file extension
b := &bytes.Buffer{}
w := &testResponseWriter{b, http.Header{}}
r := &http.Request{RequestURI: "/index.html"}
mw := m.ResponseWriter(w, r)
_, err := mw.Write([]byte("input"))
test.Error(t, mw.Close())
_, _ = mw.Write([]byte("test"))
test.Error(t, mw.Close())
test.String(t, b.String(), "test", "equal input after dummy minify response writer")
test.String(t, b.String(), "minified")
_, err = mw.Write([]byte("test"))
test.T(t, err, io.ErrClosedPipe)

// use Content-Type header
b = &bytes.Buffer{}
w = &testResponseWriter{b, http.Header{}}
r = &http.Request{RequestURI: "/index"}
mw = m.ResponseWriter(w, r)
mw.Header().Add("Content-Type", "text/html")
_, _ = mw.Write([]byte("test"))
_, _ = mw.Write([]byte("input"))
mw.WriteHeader(http.StatusForbidden)
test.Error(t, mw.Close())
test.String(t, b.String(), "test", "equal input after dummy minify response writer")
test.String(t, b.String(), "minified")

// don't minify
b = &bytes.Buffer{}
w = &testResponseWriter{b, http.Header{}}
r = &http.Request{RequestURI: "/image.png"}
mw = m.ResponseWriter(w, r)
_, _ = mw.Write([]byte("input"))
test.Error(t, mw.Close())
test.String(t, b.String(), "input")
}

func TestMiddleware(t *testing.T) {
Expand Down

0 comments on commit f9aa0ad

Please sign in to comment.