diff --git a/internal/core/unfurl/providers.go b/internal/core/unfurl/providers.go index d968a9d..46b5d2c 100644 --- a/internal/core/unfurl/providers.go +++ b/internal/core/unfurl/providers.go @@ -106,13 +106,24 @@ func fetchOEmbed(ctx context.Context, urlStr string, timeout time.Duration, user return &oembed, nil } +// normalizeURL converts protocol-relative URLs to HTTPS +// Examples: +// "//example.com/image.jpg" -> "https://example.com/image.jpg" +// "https://example.com/image.jpg" -> "https://example.com/image.jpg" (unchanged) +func normalizeURL(urlStr string) string { + if strings.HasPrefix(urlStr, "//") { + return "https:" + urlStr + } + return urlStr +} + // mapOEmbedToResult converts oEmbed response to UnfurlResult func mapOEmbedToResult(oembed *oEmbedResponse, originalURL string) *UnfurlResult { result := &UnfurlResult{ URI: originalURL, Title: oembed.Title, Description: oembed.Description, - ThumbnailURL: oembed.ThumbnailURL, + ThumbnailURL: normalizeURL(oembed.ThumbnailURL), Provider: strings.ToLower(oembed.ProviderName), Domain: extractDomain(originalURL), Width: oembed.Width, @@ -186,7 +197,7 @@ func fetchOpenGraph(ctx context.Context, urlStr string, timeout time.Duration, u URI: urlStr, Title: og.Title, Description: og.Description, - ThumbnailURL: og.Image, + ThumbnailURL: normalizeURL(og.Image), Provider: "opengraph", Domain: extractDomain(urlStr), } diff --git a/internal/core/unfurl/providers_test.go b/internal/core/unfurl/providers_test.go new file mode 100644 index 0000000..e4174b2 --- /dev/null +++ b/internal/core/unfurl/providers_test.go @@ -0,0 +1,54 @@ +package unfurl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeURL(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "protocol-relative URL", + input: "//cdn.example.com/image.jpg", + expected: "https://cdn.example.com/image.jpg", + }, + { + name: "https URL unchanged", + input: "https://example.com/image.jpg", + expected: "https://example.com/image.jpg", + }, + { + name: "http URL unchanged", + input: "http://example.com/image.jpg", + expected: "http://example.com/image.jpg", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "protocol-relative with query params", + input: "//cdn.example.com/image.jpg?width=500&height=300", + expected: "https://cdn.example.com/image.jpg?width=500&height=300", + }, + { + name: "real Streamable URL", + input: "//cdn-cf-east.streamable.com/image/7kpdft.jpg?Expires=1762932720", + expected: "https://cdn-cf-east.streamable.com/image/7kpdft.jpg?Expires=1762932720", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeURL(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} +