Skip to content

Commit

Permalink
Add MiddlewareWithError and improve error handling of Middleware slig…
Browse files Browse the repository at this point in the history
…htly, see #466
  • Loading branch information
tdewolff committed Apr 1, 2022
1 parent 4d2cb0b commit b05e7cb
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 24 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,33 @@ func main() {
m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)

fs := http.FileServer(http.Dir("www/"))
http.Handle("/", m.Middleware(fs))
http.Handle("/", m.MiddlewareWithError(fs))
}

func handleError(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
```

In order to properly handle minify errors, it is necessary to close the response writer since all writes are concurrently handled. There is no need to check errors on writes since they will be returned on closing.

```go
func main() {
m := minify.New()
m.AddFunc("text/html", html.Minify)
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)

input := `<script>const i = 1_000_</script>` // Faulty JS
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
m.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(input))

if err = w.(io.Closer).Close(); err != nil {
panic(err)
}
})).ServeHTTP(rec, req)
}
```

Expand Down
71 changes: 48 additions & 23 deletions minify.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ var Warning = log.New(os.Stderr, "WARNING: ", 0)
// ErrNotExist is returned when no minifier exists for a given mimetype.
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")

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

// MinifierFunc is a function that implements Minifer.
Expand Down Expand Up @@ -248,65 +251,74 @@ func (m *M) Reader(mediatype string, r io.Reader) io.Reader {
return pr
}

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

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

// Close must be called when writing has finished. It returns the error from the minifier.
func (w *minifyWriter) Close() error {
w.pw.Close()
w.wg.Wait()
func (w *writer) Close() error {
if !w.closed {
w.pw.Close()
w.wg.Wait()
w.closed = true
}
return w.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) *minifyWriter {
func (m *M) Writer(mediatype string, w io.Writer) *writer {
pr, pw := io.Pipe()
mw := &minifyWriter{pw, sync.WaitGroup{}, nil}
mw := &writer{pw, sync.WaitGroup{}, nil, false}
mw.wg.Add(1)
go func() {
defer mw.wg.Done()

if err := m.Minify(mediatype, w, pr); err != nil {
mw.err = err
io.Copy(w, pr)
}
pr.Close()
}()
return mw
}

// minifyResponseWriter wraps an http.ResponseWriter and makes sure that errors from the minifier are passed down through Close (can be blocking).
// responseWriter wraps an http.ResponseWriter and makes sure that errors from the minifier are passed down through Close (can be blocking).
// All writes to the response writer are intercepted and minified on the fly.
// http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ...
type minifyResponseWriter struct {
type responseWriter struct {
http.ResponseWriter

writer *minifyWriter
writer *writer
m *M
mediatype string
}

// WriteHeader intercepts any header writes and removes the Content-Length header.
func (w *minifyResponseWriter) WriteHeader(status int) {
func (w *responseWriter) WriteHeader(status int) {
w.ResponseWriter.Header().Del("Content-Length")
w.ResponseWriter.WriteHeader(status)
}

// 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 *minifyResponseWriter) Write(b []byte) (int, error) {
func (w *responseWriter) Write(b []byte) (int, error) {
if w.writer == nil {
// first write
if mediatype := w.ResponseWriter.Header().Get("Content-Type"); mediatype != "" {
Expand All @@ -318,7 +330,7 @@ func (w *minifyResponseWriter) Write(b []byte) (int, error) {
}

// Close must be called when writing has finished. It returns the error from the minifier.
func (w *minifyResponseWriter) Close() error {
func (w *responseWriter) Close() error {
if w.writer != nil {
return w.writer.Close()
}
Expand All @@ -328,9 +340,9 @@ func (w *minifyResponseWriter) Close() error {
// ResponseWriter minifies any writes to the http.ResponseWriter.
// http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ...
// Minification might be slower than just sending the original file! Caching is advised.
func (m *M) ResponseWriter(w http.ResponseWriter, r *http.Request) *minifyResponseWriter {
func (m *M) ResponseWriter(w http.ResponseWriter, r *http.Request) *responseWriter {
mediatype := mime.TypeByExtension(path.Ext(r.RequestURI))
return &minifyResponseWriter{w, nil, m, mediatype}
return &responseWriter{w, nil, m, mediatype}
}

// Middleware provides a middleware function that minifies content on the fly by intercepting writes to http.ResponseWriter.
Expand All @@ -339,8 +351,21 @@ func (m *M) ResponseWriter(w http.ResponseWriter, r *http.Request) *minifyRespon
func (m *M) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw := m.ResponseWriter(w, r)
defer mw.Close()
next.ServeHTTP(mw, r)
mw.Close()
})
}

// MiddlewareWithError provides a middleware function that minifies content on the fly by intercepting writes to http.ResponseWriter. The error function allows handling minification errors.
// http.ResponseWriter loses all functionality such as Pusher, Hijacker, Flusher, ...
// Minification might be slower than just sending the original file! Caching is advised.
func (m *M) MiddlewareWithError(next http.Handler, errorFunc func(w http.ResponseWriter, r *http.Request, err error)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw := m.ResponseWriter(w, r)
next.ServeHTTP(mw, r)
if err := mw.Close(); err != nil {
errorFunc(w, r, err)
return
}
})
}

0 comments on commit b05e7cb

Please sign in to comment.