package httpclient import ( "context" "errors" "fmt" "net/http" ) const defaultMaxRedirects = 5 // DoWithRedirect 手动跟随 3xx,跨主机跳转保留 Authorization header。 // maxRedirects == 0 禁止 redirect(等同 Do) // maxRedirects < 0 用 defaultMaxRedirects func (c *Client) DoWithRedirect(ctx context.Context, req *http.Request, maxRedirects int, allowedHosts ...string) (*http.Response, error) { if maxRedirects == 0 { return c.Do(ctx, req, allowedHosts...) } if maxRedirects < 0 { maxRedirects = defaultMaxRedirects } current := req remaining := maxRedirects for { resp, err := c.Do(ctx, current, allowedHosts...) if err != nil { return nil, err } if resp.StatusCode < 300 || resp.StatusCode >= 400 { return resp, nil } // 3xx if remaining == 0 { _ = resp.Body.Close() return nil, errors.New("httpclient: too many redirects") } remaining-- loc := resp.Header.Get("Location") if loc == "" { _ = resp.Body.Close() return nil, fmt.Errorf("httpclient: %d %s without Location header", resp.StatusCode, resp.Status) } next, err := current.URL.Parse(loc) if err != nil { _ = resp.Body.Close() return nil, fmt.Errorf("httpclient: parse redirect Location %q: %w", loc, err) } _ = resp.Body.Close() newReq, err := http.NewRequestWithContext(ctx, current.Method, next.String(), nil) if err != nil { return nil, fmt.Errorf("httpclient: build redirect request: %w", err) } // **保留** header (Authorization 等) newReq.Header = current.Header.Clone() // TODO(redirect): per RFC 9110 §15.4, 303 See Other must switch the // method to GET and drop the body; 307 Temporary Redirect and // 308 Permanent Redirect must preserve both method AND body. This // implementation always preserves the method and discards the body, // which is correct for 307/308 GET (YARN amContainerLogs) but // non-compliant for 303 and any 307/308 POST. Current YARN / SHS // API surface only triggers 307 on GET, so the gap is unfired. // TestDoWithRedirect_DeferredRFC9110Gaps locks the current behavior // so a future fix must update that test deliberately. newReq.Body = nil current = newReq } }