labstack/echo · error

ResponseWriter does not implement 'Unwrap() http.ResponseWri

Error message

ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response

What it means

Returned by echo.UnwrapResponse when the given http.ResponseWriter cannot be unwrapped down to a *echo.Response. UnwrapResponse walks the Unwrap() chain (each wrapper must implement Unwrap() http.ResponseWriter) looking for a *Response; if it hits a writer that has neither type it gives up. This helper is used internally (e.g., by the HEAD-request writer and error-handling path) but can be called by user middleware too.

Source

Thrown at response.go:130

	r.afterFuncs = nil
	r.ResponseWriter = w
	r.Size = 0
	r.Status = http.StatusOK
	r.Committed = false
}

// UnwrapResponse unwraps given ResponseWriter to return contexts original Echo Response. rw has to implement
// following method `Unwrap() http.ResponseWriter`
func UnwrapResponse(rw http.ResponseWriter) (*Response, error) {
	for {
		switch t := rw.(type) {
		case *Response:
			return t, nil
		case interface{ Unwrap() http.ResponseWriter }:
			rw = t.Unwrap()
			continue
		default:
			return nil, errors.New("ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response")
		}
	}
}

// delayedStatusWriter is a wrapper around http.ResponseWriter that delays writing the status code until first Write is called.
// This allows (global) error handler to decide correct status code to be sent to the client.
type delayedStatusWriter struct {
	http.ResponseWriter
	committed bool
	status    int
}

func (w *delayedStatusWriter) WriteHeader(statusCode int) {
	// in case something else writes status code explicitly before us we need mark response committed
	w.committed = true
	w.ResponseWriter.WriteHeader(statusCode)
}

View on GitHub (pinned to 05489dc173)

Solutions

  1. Make any custom http.ResponseWriter wrapper implement Unwrap() http.ResponseWriter returning the underlying writer, so the chain reaches *echo.Response.
  2. If you only need the *echo.Response, access it via c.Response() from the Context rather than unwrapping an arbitrary writer.
  3. Check the returned error from UnwrapResponse before using the *Response (it is a (resp, err) signature).
  4. When testing, wrap httptest.NewRecorder() in an echo.Response via echo.NewResponse(rec, logger) instead of passing the bare recorder.

Example fix

// before: custom wrapper breaks the unwrap chain
type myWriter struct{ http.ResponseWriter }
// after: implement Unwrap so ResponseController / UnwrapResponse work
type myWriter struct{ http.ResponseWriter }
func (w *myWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter }
Defensive patterns

Strategy: type-guard

Type guard

func isUnwrappable(rw http.ResponseWriter) bool {
    _, err := echo.UnwrapResponse(rw)
    return err == nil
}

// or check the interface directly:
func hasUnwrap(rw http.ResponseWriter) bool {
    _, ok := rw.(interface{ Unwrap() http.ResponseWriter })
    if !ok {
        _, ok = rw.(*echo.Response)
    }
    return ok
}

Prevention

When it happens

Trigger: Passing an http.ResponseWriter obtained outside Echo's request lifecycle, or a custom wrapper that does not implement Unwrap() (or whose Unwrap chain never reaches *echo.Response) to echo.UnwrapResponse.

Common situations: User middleware replaces c.Response() with a custom writer that does not implement Unwrap(); testing with httptest.NewRecorder() directly against code that calls UnwrapResponse; or a third-party wrapper that fails to forward Unwrap.

Related errors


AI-assisted analysis of labstack/echo@05489dc173 (2026-08-04). Data as JSON: /data/errors/95bef54f59b14171.json. Report an issue: GitHub.