package adapters import ( "context" "fmt" "io" "net/http" "os" "time" ) const ( // videoDownloadTimeout is the timeout for downloading video from URI. videoDownloadTimeout = 60 * time.Second // maxVideoSize is the maximum video size to download (500MB). maxVideoSize = 500 * 1024 * 1024 ) // downloadVideo fetches video bytes from a URL, streaming to a temp file first. // Uses explicit timeout and size limit (500MB) to prevent hangs and OOM. func downloadVideo(ctx context.Context, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Use client with explicit timeout (never use http.DefaultClient) httpClient := &http.Client{Timeout: videoDownloadTimeout} resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch video: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status %d", resp.StatusCode) } // Stream to temp file to avoid OOM on large videos tmpFile, err := os.CreateTemp("", "video-*.mp4") if err != nil { return nil, fmt.Errorf("create temp file: %w", err) } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() // Limit to maxVideoSize limitedReader := io.LimitReader(resp.Body, maxVideoSize) written, err := io.Copy(tmpFile, limitedReader) if err != nil { return nil, fmt.Errorf("write video to temp: %w", err) } if written == maxVideoSize { return nil, fmt.Errorf("video exceeds %dMB limit", maxVideoSize/(1024*1024)) } // Seek back and read if _, err := tmpFile.Seek(0, 0); err != nil { return nil, fmt.Errorf("seek temp file: %w", err) } data, err := io.ReadAll(tmpFile) if err != nil { return nil, fmt.Errorf("read temp file: %w", err) } return data, nil }