this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Use configurable timeout for external HTTP requests

This fixes a performance issue where invalid Spotify URLs would cause
the application to hang for 10+ seconds while waiting for the oEmbed
API to timeout (returning a 504).

+81 -49
+7 -2
README.md
··· 86 86 database: tumble.db 87 87 baseurl: your.domain.com 88 88 port: 8080 89 + request_timeout: 2s 89 90 ``` 90 91 91 92 **For MySQL:** ··· 110 111 - `TUMBLE_MODE=development` (Options: `development`, `production`. Default: `production`) 111 112 - `TUMBLE_EMBED_ASSETS=true` (Options: `true`, `false`. Default: `true`) 112 113 - `TUMBLE_LOGGING_LEVEL=debug` 114 + - `TUMBLE_REQUEST_TIMEOUT=2s` (Default: `2s`) 113 115 114 116 ### Environment Modes (`TUMBLE_MODE`) 115 117 ··· 130 132 This setting is independent of `TUMBLE_MODE`, allowing you to run in development mode (for verbose logging and detailed errors) while still using embedded assets for deployment. 131 133 132 134 **Deployment Example:** 135 + 133 136 ```yaml 134 - mode: development # Get detailed error messages and text logging 135 - embed_assets: true # But still use embedded assets (no source checkout needed) 137 + mode: development # Get detailed error messages and text logging 138 + embed_assets: true # But still use embedded assets (no source checkout needed) 136 139 ``` 137 140 138 141 ### 2. Initialize Database ··· 282 285 - **HTML Response**: Shows duplicate notification with original poster and timestamp 283 286 284 287 **Example JSON Response (Duplicate):** 288 + 285 289 ```json 286 290 { 287 291 "link_id": 456, ··· 324 328 ``` 325 329 326 330 **Responses:** 331 + 327 332 - **200 OK**: Link deleted successfully 328 333 - **400 Bad Request**: Missing or invalid ID 329 334 - **403 Forbidden**: Missing or invalid admin secret
+16 -13
internal/config/config.go
··· 3 3 import ( 4 4 "fmt" 5 5 "strings" 6 + "time" 6 7 7 8 "github.com/spf13/viper" 8 9 ) 9 10 10 11 type Config struct { 11 - Host string `yaml:"host" mapstructure:"host"` 12 - Database string `yaml:"database" mapstructure:"database"` 13 - Username string `yaml:"username" mapstructure:"username"` 14 - Password string `yaml:"password" mapstructure:"password"` 15 - BaseURL string `yaml:"baseurl" mapstructure:"baseurl"` 16 - Driver string `yaml:"driver" mapstructure:"driver"` 17 - Port string `yaml:"port" mapstructure:"port"` 18 - Mode string `yaml:"mode" mapstructure:"mode"` 19 - EmbedAssets bool `yaml:"embed_assets" mapstructure:"embed_assets"` 20 - ClickSigningKey string `yaml:"click_signing_key" mapstructure:"click_signing_key"` 21 - AdminSecret string `yaml:"admin_secret" mapstructure:"admin_secret"` 22 - Logging Logging `yaml:"logging" mapstructure:"logging"` 23 - Caching Caching `yaml:"caching" mapstructure:"caching"` 12 + Host string `yaml:"host" mapstructure:"host"` 13 + Database string `yaml:"database" mapstructure:"database"` 14 + Username string `yaml:"username" mapstructure:"username"` 15 + Password string `yaml:"password" mapstructure:"password"` 16 + BaseURL string `yaml:"baseurl" mapstructure:"baseurl"` 17 + Driver string `yaml:"driver" mapstructure:"driver"` 18 + Port string `yaml:"port" mapstructure:"port"` 19 + Mode string `yaml:"mode" mapstructure:"mode"` 20 + EmbedAssets bool `yaml:"embed_assets" mapstructure:"embed_assets"` 21 + ClickSigningKey string `yaml:"click_signing_key" mapstructure:"click_signing_key"` 22 + AdminSecret string `yaml:"admin_secret" mapstructure:"admin_secret"` 23 + Logging Logging `yaml:"logging" mapstructure:"logging"` 24 + Caching Caching `yaml:"caching" mapstructure:"caching"` 25 + RequestTimeout time.Duration `yaml:"request_timeout" mapstructure:"request_timeout"` 24 26 } 25 27 26 28 type Caching struct { ··· 43 45 v.SetDefault("logging.level", "info") 44 46 v.SetDefault("logging.output", "stdout") 45 47 v.SetDefault("caching.enabled", true) 48 + v.SetDefault("request_timeout", "2s") 46 49 47 50 // Environment Variables 48 51 v.SetEnvPrefix("TUMBLE")
+6 -2
internal/handler/preview.go
··· 168 168 // Use strict browser UA to avoid bot detection (Kickstarter, TikTok, etc.) 169 169 req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") 170 170 171 - client := &http.Client{} 171 + client := &http.Client{ 172 + Timeout: h.Config.RequestTimeout, 173 + } 172 174 resp, err := client.Do(req) 173 175 if err != nil { 174 176 return nil, err ··· 265 267 } 266 268 req.Header.Set("User-Agent", userAgent) 267 269 268 - client := &http.Client{} 270 + client := &http.Client{ 271 + Timeout: h.Config.RequestTimeout, 272 + } 269 273 resp, err := client.Do(req) 270 274 if err != nil { 271 275 return nil, err
+3 -1
internal/handler/preview_flickr.go
··· 72 72 } 73 73 req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") 74 74 75 - client := &http.Client{} 75 + client := &http.Client{ 76 + Timeout: h.Config.RequestTimeout, 77 + } 76 78 resp, err := client.Do(req) 77 79 if err != nil { 78 80 return "", err
+3 -1
internal/handler/preview_reddit.go
··· 57 57 // Use generic browser UA 58 58 req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") 59 59 60 - client := &http.Client{} 60 + client := &http.Client{ 61 + Timeout: h.Config.RequestTimeout, 62 + } 61 63 resp, err := client.Do(req) 62 64 if err != nil { 63 65 return nil, err
+3 -1
internal/service/content.go
··· 320 320 req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") 321 321 322 322 // Make request 323 - client := &http.Client{} 323 + client := &http.Client{ 324 + Timeout: s.Config.RequestTimeout, 325 + } 324 326 resp, err := client.Do(req) 325 327 if err != nil { 326 328 return ""
+43 -29
tests/load_fixtures.sh
··· 28 28 echo "Using BASE_URL: $BASE_URL" 29 29 echo "Using DB_PATH: $DB_PATH" 30 30 31 + TOTAL_FIXTURES=24 32 + CURRENT=0 33 + 34 + load_link() { 35 + CURRENT=$((CURRENT + 1)) 36 + echo " [$CURRENT/$TOTAL_FIXTURES] Adding link: $1" 37 + $ADD_LINK_SCRIPT "$1" "$2" 38 + } 39 + 40 + load_quote() { 41 + CURRENT=$((CURRENT + 1)) 42 + echo " [$CURRENT/$TOTAL_FIXTURES] Adding quote: $1" 43 + $ADD_QUOTE_SCRIPT "$1" "$2" 44 + } 45 + 46 + ADD_QUOTE_SCRIPT="./tests/add_quote.sh" 47 + 31 48 echo "Loading fixtures..." 32 49 33 50 # YouTube Video 34 - $ADD_LINK_SCRIPT "video_fan" "https://youtu.be/rgDcbP4Hem4?si=YdwaAMNTiD9PXKzH" 51 + load_link "video_fan" "https://youtu.be/rgDcbP4Hem4?si=YdwaAMNTiD9PXKzH" 35 52 36 53 # Image 37 - $ADD_LINK_SCRIPT "pic_poster" "https://cdn.tinnies.club/accounts/avatars/109/626/500/076/902/223/original/2a4a0d1a4ce728c4.jpg" 54 + load_link "pic_poster" "https://cdn.tinnies.club/accounts/avatars/109/626/500/076/902/223/original/2a4a0d1a4ce728c4.jpg" 38 55 39 56 # Mastodon Pos 40 - $ADD_LINK_SCRIPT "social_butterfly" "https://fosstodon.org/@genebean/113945244453254504" 57 + load_link "social_butterfly" "https://fosstodon.org/@genebean/113945244453254504" 41 58 42 59 # Standard Link 43 - $ADD_LINK_SCRIPT "web_surfer" "http://costs.wtf" 60 + load_link "web_surfer" "http://costs.wtf" 44 61 45 62 # Reddit Post (Valheim) 46 - $ADD_LINK_SCRIPT "gamer_girl" "https://www.reddit.com/r/valheim/comments/leqdj6/our_first_encounter_with_the_troll/" 63 + load_link "gamer_girl" "https://www.reddit.com/r/valheim/comments/leqdj6/our_first_encounter_with_the_troll/" 47 64 48 65 # Imgur (Animated) 49 - $ADD_LINK_SCRIPT "meme_lord" "https://imgur.com/only-one-jack-black-0qetp3u" 66 + load_link "meme_lord" "https://imgur.com/only-one-jack-black-0qetp3u" 50 67 51 68 # Twitter Pos 52 - $ADD_LINK_SCRIPT "tweet_master" "https://x.com/jcockhren/status/1229101594505097216?s=20" 69 + load_link "tweet_master" "https://x.com/jcockhren/status/1229101594505097216?s=20" 53 70 54 71 # Wikipedia (Go) 55 - $ADD_LINK_SCRIPT "knowledge_seeker" "https://en.wikipedia.org/wiki/Go_(programming_language)" 72 + load_link "knowledge_seeker" "https://en.wikipedia.org/wiki/Go_(programming_language)" 56 73 57 74 # Broken Link (404) 58 - $ADD_LINK_SCRIPT "404_finder" "http://google.com/this-page-does-not-exist-12345" 75 + load_link "404_finder" "http://google.com/this-page-does-not-exist-12345" 59 76 60 77 # Another Explicit 404 Link (Test Case) 61 - $ADD_LINK_SCRIPT "broken_link_tester" "http://httpstat.us/404" 62 - 78 + load_link "broken_link_tester" "http://httpstat.us/404" 63 79 64 80 # Unavailable Video (Soft 404) 65 - $ADD_LINK_SCRIPT "video_gone" "https://youtu.be/Ie_Wl9eNffE" 81 + load_link "video_gone" "https://youtu.be/Ie_Wl9eNffE" 66 82 67 83 # Valid Video (Rick Roll) 68 - $ADD_LINK_SCRIPT "astley_fan" "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 69 - 70 - ADD_QUOTE_SCRIPT="./tests/add_quote.sh" 84 + load_link "astley_fan" "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 71 85 72 86 # Quotes 73 - $ADD_QUOTE_SCRIPT "Linus Torvalds" "Talk is cheap. Show me the code." 74 - $ADD_QUOTE_SCRIPT "Brian Kernighan" "Debugging is twice as hard as writing the code in the first place." 75 - $ADD_QUOTE_SCRIPT "Simba" "Everything the light touches is our kingdom." 87 + load_quote "Linus Torvalds" "Talk is cheap. Show me the code." 88 + load_quote "Brian Kernighan" "Debugging is twice as hard as writing the code in the first place." 89 + load_quote "Simba" "Everything the light touches is our kingdom." 76 90 77 91 # Spotify 78 - $ADD_LINK_SCRIPT "music_lover" "https://open.spotify.com/episode/7makk4oTQel546B0PZlDM5" 92 + load_link "music_lover" "https://open.spotify.com/episode/7makk4oTQel546B0PZlDM5" 79 93 80 94 # TikTok 81 - $ADD_LINK_SCRIPT "tiktok_star" "https://www.tiktok.com/@tiagogreis/video/6830059644233223429" 95 + load_link "tiktok_star" "https://www.tiktok.com/@tiagogreis/video/6830059644233223429" 82 96 83 97 # Flickr 84 - $ADD_LINK_SCRIPT "photog" "http://flickr.com/photos/bees/2362225867/" 98 + load_link "photog" "http://flickr.com/photos/bees/2362225867/" 85 99 86 100 # Instagram 87 - $ADD_LINK_SCRIPT "insta_fan" "https://www.instagram.com/p/fA9uwTtkSN/" 101 + load_link "insta_fan" "https://www.instagram.com/p/fA9uwTtkSN/" 88 102 89 103 # Dailymotion 90 - $ADD_LINK_SCRIPT "video_daily" "https://www.dailymotion.com/video/x7tgad0" 104 + load_link "video_daily" "https://www.dailymotion.com/video/x7tgad0" 91 105 92 106 # Kickstarter 93 - $ADD_LINK_SCRIPT "backer" "https://www.kickstarter.com/projects/ouya/ouya-a-new-kind-of-video-game-console" 107 + load_link "backer" "https://www.kickstarter.com/projects/ouya/ouya-a-new-kind-of-video-game-console" 94 108 95 109 # SlideShare 96 - $ADD_LINK_SCRIPT "presenter" "http://www.slideshare.net/lyndadotcom/code-drivesworld12" 110 + load_link "presenter" "http://www.slideshare.net/lyndadotcom/code-drivesworld12" 97 111 98 112 # Speaker Deck 99 - $ADD_LINK_SCRIPT "speaker" "https://speakerdeck.com/mislav/git" 113 + load_link "speaker" "https://speakerdeck.com/mislav/git" 100 114 101 115 # Giphy 102 - $ADD_LINK_SCRIPT "gif_master" "https://giphy.com/gifs/cant-hardly-wait-kW8mnYSNkUYKc" 116 + load_link "gif_master" "https://giphy.com/gifs/cant-hardly-wait-kW8mnYSNkUYKc" 103 117 104 118 # Kevin's Broken Twitter Link (Sad Path) 105 119 # Use the specific broken ID if possible, but add_link auto-increments. 106 120 # We will just assert on content behavior for "kevin". 107 - $ADD_LINK_SCRIPT "kevin" "https://twitter.com/darkuncle/status/1483507577174441985" 121 + load_link "kevin" "https://twitter.com/darkuncle/status/1483507577174441985" 108 122 109 123 # Tester's Valid Twitter Link (Happy Path) 110 - $ADD_LINK_SCRIPT "tester" "https://twitter.com/jcockhren/status/1229101594505097216?s=20" 124 + load_link "tester" "https://twitter.com/jcockhren/status/1229101594505097216?s=20" 111 125 112 126 echo "Loading backdated 'Hot Links' directly into DB..." 113 127 if [ "$DRIVER" == "mysql" ]; then