labstack/echo · warning

proxy raw, copy body error=%w, url=%s

Error message

proxy raw, copy body error=%w, url=%s

What it means

Stored in context (_error) by proxyRaw() when the first io.Copy goroutine — copying from the inbound (client) connection 'in' to the upstream 'out' — returns a non-EOF error during raw (WebSocket) proxying. This represents the client-to-upstream direction failing mid-stream. The error is only recorded if err1 is not nil and not io.EOF.

Source

Thrown at middleware/proxy.go:181

			c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", err, t.URL)))
			return
		}

		errCh := make(chan error, 2)
		cp := func(dst io.Writer, src io.Reader) {
			_, copyErr := io.Copy(dst, src)
			errCh <- copyErr
		}

		go cp(out, in)
		go cp(in, out)

		// Wait for BOTH goroutines to complete
		err1 := <-errCh
		err2 := <-errCh

		if err1 != nil && err1 != io.EOF {
			c.Set("_error", fmt.Errorf("proxy raw, copy body error=%w, url=%s", err1, t.URL))
		} else if err2 != nil && err2 != io.EOF {
			c.Set("_error", fmt.Errorf("proxy raw, copy body error=%w, url=%s", err2, t.URL))
		}
	})
}

// NewRandomBalancer returns a random proxy balancer.
func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
	b := randomBalancer{}
	b.targets = targets
	// G404 (CWE-338): Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand)
	// this random is used to select next target. I can not think of reason this must be cryptographically safe. If you can - please open PR.
	b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) // #nosec G404
	return &b
}

// NewRoundRobinBalancer returns a round-robin proxy balancer.
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {

View on GitHub (pinned to 05489dc173)

Solutions

  1. Verify network connectivity and stability between client, proxy, and upstream.
  2. Check that the upstream WebSocket endpoint accepts and keeps the connection open.
  3. Add ProxyConfig.RetryCount and a RetryFilter to retry on transient upstream failures.
  4. Inspect the wrapped error (err1) to distinguish client-side vs upstream-side disconnects for observability.
Defensive patterns

Strategy: retry

Try / catch

// After the proxy middleware runs, inspect the context error and decide retry/response.
if errVal, ok := c.Get("_error").(error); ok && errVal != nil {
    if errors.Is(errVal, io.ErrUnexpectedEOF) || isTransient(errVal) {
        // optionally retry via ProxyConfig.RetryCount instead
    }
    c.Error(errVal)
}

Prevention

When it happens

Trigger: During a WebSocket/raw proxied session, the client side disconnects abruptly, the upstream closes early, or the network drops while bytes flow from client to upstream. The proxy waits for both copy goroutines, and if the client->upstream copy errors, this message is set.

Common situations: Client closes the WebSocket without a close frame; network interruption between proxy and client; upstream rejecting the connection after upgrade; half-open TCP connections after a NAT timeout.

Related errors


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