Skip to content

Commit 80522a1

Browse files
fix: rewrite download URL host to match HARNESS_BASE_URL for TLS compatibility
Fixes download_execution_logs TLS certificate errors when HARNESS_BASE_URL is set to a custom/vanity domain (e.g., mycompany.harness.io). The /blob/download API returns pre-signed URLs hardcoded to app.harness.io. This rewrites the host to match the configured base URL and uses the configured HTTP client with its TLS/proxy settings instead of bare http.Get(). Fixes #39 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6379d69 commit 80522a1

File tree

4 files changed

+124
-3
lines changed

4 files changed

+124
-3
lines changed

common/client/client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,13 @@ func (c *Client) RequestRaw(
567567
return nil
568568
}
569569

570+
// HTTPClient returns the underlying *http.Client used by this Client.
571+
// This is useful when you need to make HTTP requests that don't require
572+
// auth header injection but should share the same transport/TLS configuration.
573+
func (c *Client) HTTPClient() *http.Client {
574+
return c.client
575+
}
576+
570577
// Do is a wrapper of http.Client.Do that injects the auth header in the request.
571578
func (c *Client) Do(r *http.Request) (*http.Response, error) {
572579
slog.DebugContext(r.Context(), "Request", "method", r.Method, "url", r.URL.String())

common/client/logs.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"log/slog"
7+
"net/url"
78
"time"
89

910
"github.com/harness/mcp-server/common/client/dto"
@@ -99,5 +100,42 @@ func (l *LogService) GetDownloadLogsURL(ctx context.Context, scope dto.Scope, pl
99100
return "", lastErr
100101
}
101102

102-
return response.Link, nil
103+
// Rewrite the download URL host to match the configured base URL.
104+
// The API may return a pre-signed URL with a hardcoded host (e.g., app.harness.io)
105+
// that differs from HARNESS_BASE_URL, causing TLS failures in environments with
106+
// custom domains or corporate proxies.
107+
rewrittenLink, err := rewriteDownloadURLHost(response.Link, l.LogServiceClient.BaseURL)
108+
if err != nil {
109+
slog.WarnContext(ctx, "Failed to rewrite download URL host, using original URL", "error", err)
110+
return response.Link, nil
111+
}
112+
113+
return rewrittenLink, nil
114+
}
115+
116+
// rewriteDownloadURLHost replaces the host of a download URL with the host from the
117+
// configured base URL, so that the download request routes through the same trusted
118+
// endpoint as all other API calls.
119+
func rewriteDownloadURLHost(downloadLink string, baseURL *url.URL) (string, error) {
120+
if baseURL == nil {
121+
return downloadLink, nil
122+
}
123+
124+
parsed, err := url.Parse(downloadLink)
125+
if err != nil {
126+
return "", fmt.Errorf("failed to parse download URL: %w", err)
127+
}
128+
129+
// Only rewrite if the hosts actually differ
130+
if parsed.Host == baseURL.Host {
131+
return downloadLink, nil
132+
}
133+
134+
slog.Info("Rewriting download URL host to match configured base URL",
135+
"original_host", parsed.Host,
136+
"new_host", baseURL.Host)
137+
138+
parsed.Scheme = baseURL.Scheme
139+
parsed.Host = baseURL.Host
140+
return parsed.String(), nil
103141
}

common/client/logs_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package client
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
)
7+
8+
func TestRewriteDownloadURLHost(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
downloadLink string
12+
baseURL string
13+
wantURL string
14+
wantErr bool
15+
}{
16+
{
17+
name: "rewrites app.harness.io to custom domain",
18+
downloadLink: "https://app.harness.io/storage/harness-download/logs/abc123?token=xyz",
19+
baseURL: "https://mycompany.harness.io",
20+
wantURL: "https://mycompany.harness.io/storage/harness-download/logs/abc123?token=xyz",
21+
},
22+
{
23+
name: "no rewrite when hosts match",
24+
downloadLink: "https://app.harness.io/storage/logs/abc123",
25+
baseURL: "https://app.harness.io",
26+
wantURL: "https://app.harness.io/storage/logs/abc123",
27+
},
28+
{
29+
name: "nil base URL returns original link",
30+
downloadLink: "https://app.harness.io/storage/logs/abc123",
31+
baseURL: "",
32+
wantURL: "https://app.harness.io/storage/logs/abc123",
33+
},
34+
{
35+
name: "preserves query parameters",
36+
downloadLink: "https://app.harness.io/storage/logs?accountID=abc&prefix=key&X-Amz-Signature=sig123",
37+
baseURL: "https://mycompany.harness.io",
38+
wantURL: "https://mycompany.harness.io/storage/logs?accountID=abc&prefix=key&X-Amz-Signature=sig123",
39+
},
40+
{
41+
name: "preserves path structure",
42+
downloadLink: "https://app.harness.io/storage/harness-download/comp-log-service/deep/nested/path.zip",
43+
baseURL: "https://custom.example.com",
44+
wantURL: "https://custom.example.com/storage/harness-download/comp-log-service/deep/nested/path.zip",
45+
},
46+
}
47+
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
var baseURL *url.URL
51+
if tt.baseURL != "" {
52+
var err error
53+
baseURL, err = url.Parse(tt.baseURL)
54+
if err != nil {
55+
t.Fatalf("failed to parse base URL: %v", err)
56+
}
57+
}
58+
59+
got, err := rewriteDownloadURLHost(tt.downloadLink, baseURL)
60+
if (err != nil) != tt.wantErr {
61+
t.Errorf("rewriteDownloadURLHost() error = %v, wantErr %v", err, tt.wantErr)
62+
return
63+
}
64+
if got != tt.wantURL {
65+
t.Errorf("rewriteDownloadURLHost() = %v, want %v", got, tt.wantURL)
66+
}
67+
})
68+
}
69+
}

common/pkg/tools/logs.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func NewExtendedLogService(logService *client.LogService) *ExtendedLogService {
5656
type DownloadLogsConfig struct {
5757
MaxLogLines int // Enforce max lines (0 = use request parameter)
5858
GetDownloadURL func(ctx context.Context, scope dto.Scope, planExecutionID string, logKey string) (string, error) // Custom URL fetching logic
59+
HTTPClient *http.Client // HTTP client for downloading logs (inherits TLS/proxy config)
5960
}
6061

6162
// DefaultDownloadLogsConfig returns the default configuration for downloading logs
@@ -67,6 +68,7 @@ func (l *ExtendedLogService) DefaultDownloadLogsConfig() *DownloadLogsConfig {
6768
headers := map[string]string{}
6869
return l.GetDownloadLogsURL(ctx, scope, planExecutionID, logKey, headers)
6970
},
71+
HTTPClient: l.LogService.LogServiceClient.HTTPClient(),
7072
}
7173
}
7274

@@ -451,8 +453,13 @@ func DownloadExecutionLogsTool(config *config.McpServerConfig, client LogService
451453
return mcp.NewToolResultError(fmt.Sprintf("failed to fetch log download URL: %v", err)), nil
452454
}
453455

454-
// Download the logs into outputPath
455-
resp, err := http.Get(logDownloadURL)
456+
// Download the logs using the configured HTTP client (inherits TLS/proxy settings)
457+
// instead of http.Get which uses the default client with no TLS customization.
458+
httpClient := downloadLogsConfig.HTTPClient
459+
if httpClient == nil {
460+
httpClient = http.DefaultClient
461+
}
462+
resp, err := httpClient.Get(logDownloadURL)
456463
if err != nil {
457464
return mcp.NewToolResultError(fmt.Sprintf("failed to download logs: %v", err)), nil
458465
}

0 commit comments

Comments
 (0)