fetcher.go RawHistoryFolder
1// Package favicon provides favicon fetching and caching infrastructure.
2package favicon
3
4import (
5 "context"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "time"
11
12 "github.com/bnema/dumber/internal/logging"
13)
14
15const (
16 // DuckDuckGo favicon API URL template.
17 duckduckgoFaviconURL = "https://icons.duckduckgo.com/ip3/%s.ico"
18 // HTTP client timeout for favicon fetch.
19 fetchTimeout = 5 * time.Second
20)
21
22// Fetcher retrieves favicons from external APIs.
23type Fetcher struct {
24 client *http.Client
25}
26
27// NewFetcher creates a new Fetcher with default HTTP client settings.
28func NewFetcher() *Fetcher {
29 return &Fetcher{
30 client: &http.Client{
31 Timeout: fetchTimeout,
32 },
33 }
34}
35
36// Fetch retrieves favicon bytes for a domain from DuckDuckGo's favicon API.
37// Returns nil bytes if the favicon cannot be fetched.
38func (f *Fetcher) Fetch(ctx context.Context, domain string) ([]byte, error) {
39 if domain == "" {
40 return nil, nil
41 }
42
43 log := logging.FromContext(ctx)
44 faviconURL := fmt.Sprintf(duckduckgoFaviconURL, url.QueryEscape(domain))
45
46 log.Debug().Str("url", faviconURL).Msg("fetching favicon from DuckDuckGo")
47
48 req, err := http.NewRequestWithContext(ctx, http.MethodGet, faviconURL, http.NoBody)
49 if err != nil {
50 log.Debug().Err(err).Msg("failed to create favicon request")
51 return nil, err
52 }
53
54 resp, err := f.client.Do(req)
55 if err != nil {
56 log.Debug().Err(err).Str("domain", domain).Msg("failed to fetch favicon from DuckDuckGo")
57 return nil, err
58 }
59 defer func() {
60 if closeErr := resp.Body.Close(); closeErr != nil {
61 log.Debug().Err(closeErr).Str("domain", domain).Msg("failed to close favicon response body")
62 }
63 }()
64
65 if resp.StatusCode != http.StatusOK {
66 log.Debug().Int("status", resp.StatusCode).Str("domain", domain).Msg("DuckDuckGo favicon API returned non-OK status")
67 return nil, nil
68 }
69
70 data, err := io.ReadAll(resp.Body)
71 if err != nil {
72 log.Debug().Err(err).Str("domain", domain).Msg("failed to read favicon response")
73 return nil, err
74 }
75
76 if len(data) == 0 {
77 log.Debug().Str("domain", domain).Msg("empty favicon response from DuckDuckGo")
78 return nil, nil
79 }
80
81 log.Debug().Str("domain", domain).Int("bytes", len(data)).Msg("favicon fetched from DuckDuckGo")
82 return data, nil
83}