From 4d8baf71750e715cc5544a7e2c2066bb534ae635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 14 Mar 2026 18:36:00 +0800 Subject: [PATCH] ccm: fix nil pointer in pollUsage for connector-mode credentials Connector-mode credentials (URL + reverse: true) never assigned httpClient, causing a nil dereference when pollUsage accessed httpClient.Transport. Also extract poll request logic into doPollUsageRequest to try reverse transport first (single attempt), then fall back to forward transport with retries if the reverse session disconnects. --- service/ccm/credential_external.go | 66 +++++++++++++++++++----------- service/ocm/credential_external.go | 66 +++++++++++++++++++----------- 2 files changed, 86 insertions(+), 46 deletions(-) diff --git a/service/ccm/credential_external.go b/service/ccm/credential_external.go index e8fe31799..97da6bd9e 100644 --- a/service/ccm/credential_external.go +++ b/service/ccm/credential_external.go @@ -192,6 +192,7 @@ func newExternalCredential(ctx context.Context, tag string, options option.CCMEx Time: ntp.TimeFuncFromContext(ctx), } } + cred.httpClient = &http.Client{Transport: transport} } else { // Normal mode: standard HTTP client for proxying cred.httpClient = &http.Client{Transport: transport} @@ -498,6 +499,47 @@ func (c *externalCredential) interruptConnections() { } } +func (c *externalCredential) doPollUsageRequest(ctx context.Context) (*http.Response, error) { + buildRequest := func(baseURL string) func() (*http.Request, error) { + return func() (*http.Request, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/ccm/v1/status", nil) + if err != nil { + return nil, err + } + request.Header.Set("Authorization", "Bearer "+c.token) + return request, nil + } + } + // Try reverse transport first (single attempt, no retry) + if c.reverseHttpClient != nil { + session := c.getReverseSession() + if session != nil && !session.IsClosed() { + request, err := buildRequest(reverseProxyBaseURL)() + if err != nil { + return nil, err + } + reverseClient := &http.Client{ + Transport: c.reverseHttpClient.Transport, + Timeout: 5 * time.Second, + } + response, err := reverseClient.Do(request) + if err == nil { + return response, nil + } + // Reverse failed, fall through to forward if available + } + } + // Forward transport with retries + if c.httpClient != nil { + forwardClient := &http.Client{ + Transport: c.httpClient.Transport, + Timeout: 5 * time.Second, + } + return doHTTPWithRetry(ctx, forwardClient, buildRequest(c.baseURL)) + } + return nil, E.New("no transport available") +} + func (c *externalCredential) pollUsage(ctx context.Context) { if !c.pollAccess.TryLock() { return @@ -505,29 +547,7 @@ func (c *externalCredential) pollUsage(ctx context.Context) { defer c.pollAccess.Unlock() defer c.markUsagePollAttempted() - activeBaseURL := c.baseURL - activeTransport := c.httpClient.Transport - if c.reverseHttpClient != nil { - session := c.getReverseSession() - if session != nil && !session.IsClosed() { - activeBaseURL = reverseProxyBaseURL - activeTransport = c.reverseHttpClient.Transport - } - } - statusURL := activeBaseURL + "/ccm/v1/status" - httpClient := &http.Client{ - Transport: activeTransport, - Timeout: 5 * time.Second, - } - - response, err := doHTTPWithRetry(ctx, httpClient, func() (*http.Request, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil) - if err != nil { - return nil, err - } - request.Header.Set("Authorization", "Bearer "+c.token) - return request, nil - }) + response, err := c.doPollUsageRequest(ctx) if err != nil { if !c.isPollBackoffAtCap() { c.logger.Error("poll usage for ", c.tag, ": ", err) diff --git a/service/ocm/credential_external.go b/service/ocm/credential_external.go index 0b60cff1e..fbe8f11c6 100644 --- a/service/ocm/credential_external.go +++ b/service/ocm/credential_external.go @@ -211,6 +211,7 @@ func newExternalCredential(ctx context.Context, tag string, options option.OCMEx Time: ntp.TimeFuncFromContext(ctx), } } + cred.httpClient = &http.Client{Transport: transport} } else { // Normal mode: standard HTTP client for proxying cred.credDialer = credentialDialer @@ -537,6 +538,47 @@ func (c *externalCredential) interruptConnections() { } } +func (c *externalCredential) doPollUsageRequest(ctx context.Context) (*http.Response, error) { + buildRequest := func(baseURL string) func() (*http.Request, error) { + return func() (*http.Request, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/ocm/v1/status", nil) + if err != nil { + return nil, err + } + request.Header.Set("Authorization", "Bearer "+c.token) + return request, nil + } + } + // Try reverse transport first (single attempt, no retry) + if c.reverseHttpClient != nil { + session := c.getReverseSession() + if session != nil && !session.IsClosed() { + request, err := buildRequest(reverseProxyBaseURL)() + if err != nil { + return nil, err + } + reverseClient := &http.Client{ + Transport: c.reverseHttpClient.Transport, + Timeout: 5 * time.Second, + } + response, err := reverseClient.Do(request) + if err == nil { + return response, nil + } + // Reverse failed, fall through to forward if available + } + } + // Forward transport with retries + if c.httpClient != nil { + forwardClient := &http.Client{ + Transport: c.httpClient.Transport, + Timeout: 5 * time.Second, + } + return doHTTPWithRetry(ctx, forwardClient, buildRequest(c.baseURL)) + } + return nil, E.New("no transport available") +} + func (c *externalCredential) pollUsage(ctx context.Context) { if !c.pollAccess.TryLock() { return @@ -544,29 +586,7 @@ func (c *externalCredential) pollUsage(ctx context.Context) { defer c.pollAccess.Unlock() defer c.markUsagePollAttempted() - activeBaseURL := c.baseURL - activeTransport := c.httpClient.Transport - if c.reverseHttpClient != nil { - session := c.getReverseSession() - if session != nil && !session.IsClosed() { - activeBaseURL = reverseProxyBaseURL - activeTransport = c.reverseHttpClient.Transport - } - } - statusURL := activeBaseURL + "/ocm/v1/status" - httpClient := &http.Client{ - Transport: activeTransport, - Timeout: 5 * time.Second, - } - - response, err := doHTTPWithRetry(ctx, httpClient, func() (*http.Request, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil) - if err != nil { - return nil, err - } - request.Header.Set("Authorization", "Bearer "+c.token) - return request, nil - }) + response, err := c.doPollUsageRequest(ctx) if err != nil { if !c.isPollBackoffAtCap() { c.logger.Error("poll usage for ", c.tag, ": ", err)