Repository: caddyserver/caddy
File: modules/caddyhttp/reverseproxy/reverseproxy.go (Go)

Code before:
func (h *Handler) Cleanup() error {
	err := h.cleanupConnections()

	// remove hosts from our config from the pool
	for _, upstream := range h.Upstreams {
		_, _ = hosts.Delete(upstream.String())
	}

	return err
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

	// prepare the request for proxying; this is needed only once
	clonedReq, err := h.prepareRequest(r, repl)
	if err != nil {
		return caddyhttp.Error(http.StatusInternalServerError,
			fmt.Errorf("preparing request for upstream round-trip: %v", err))
	}

	// websocket over http2 or http3, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
	// TODO: once we can reliably detect backend support this, it can be removed for those backends
	if (r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket") ||
		(r.ProtoMajor == 3 && r.Method == http.MethodConnect && r.Proto == "websocket") {
		clonedReq.Header.Del(":protocol")
		// keep the body for later use. http1.1 upgrade uses http.NoBody
		caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body)
		clonedReq.Body = http.NoBody
		clonedReq.Method = http.MethodGet
		clonedReq.Header.Set("Upgrade", "websocket")
		clonedReq.Header.Set("Connection", "Upgrade")
		key := make([]byte, 16)
		_, randErr := rand.Read(key)
		if randErr != nil {
			return randErr
		}
		clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
	}

	// we will need the original headers and Host value if
	// header operations are configured; this is so that each
	// retry can apply the modifications, because placeholders
	// may be used which depend on the selected upstream for
	// their values
	reqHost := clonedReq.Host
	reqHeader := clonedReq.Header

	start := time.Now()
	defer func() {
		// total proxying duration, including time spent on LB and retries

Review comment (suggestion):
h2 and h3 both uses `:protocol` pseudo header to support extended connect. Document that h3 promotes this header to `Proto` field.

Code after:
func (h *Handler) Cleanup() error {
	err := h.cleanupConnections()

	// remove hosts from our config from the pool
	for _, upstream := range h.Upstreams {
		_, _ = hosts.Delete(upstream.String())
	}

	return err
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

	// prepare the request for proxying; this is needed only once
	clonedReq, err := h.prepareRequest(r, repl)
	if err != nil {
		return caddyhttp.Error(http.StatusInternalServerError,
			fmt.Errorf("preparing request for upstream round-trip: %v", err))
	}

	// websocket over http2 or http3 if extended connect is enabled, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
	// Both use the same upgrade mechanism: server advertizes extended connect support, and client sends the pseudo header :protocol in a CONNECT request
	// The quic-go http3 implementation also puts :protocol in r.Proto for CONNECT requests (quic-go/http3/headers.go@70-72,185,203)
	// TODO: once we can reliably detect backend support this, it can be removed for those backends
	if (r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket") ||
		(r.ProtoMajor == 3 && r.Method == http.MethodConnect && r.Proto == "websocket") {
		clonedReq.Header.Del(":protocol")
		// keep the body for later use. http1.1 upgrade uses http.NoBody
		caddyhttp.SetVar(clonedReq.Context(), "extended_connect_websocket_body", clonedReq.Body)
		clonedReq.Body = http.NoBody
		clonedReq.Method = http.MethodGet
		clonedReq.Header.Set("Upgrade", "websocket")
		clonedReq.Header.Set("Connection", "Upgrade")
		key := make([]byte, 16)
		_, randErr := rand.Read(key)
		if randErr != nil {
			return randErr
		}
		clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
	}

	// we will need the original headers and Host value if
	// header operations are configured; this is so that each
	// retry can apply the modifications, because placeholders
	// may be used which depend on the selected upstream for
	// their values
	reqHost := clonedReq.Host
	reqHeader := clonedReq.Header

	start := time.Now()