From b0a2e69711de0d8d03ec3a1331958c2fd04d36d3 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 7 Jun 2026 15:42:53 +0900 Subject: [PATCH] =?UTF-8?q?fix(youtube):=20YouTube=20API=20fetch=EC=97=90?= =?UTF-8?q?=20=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 업스트림 행 시 봇 동기화/요청 핸들러가 무한 대기하던 문제 방지. fetchWithTimeout(AbortController, 10s)으로 7개 fetch 호출 일괄 래핑. Co-Authored-By: Claude Opus 4.7 --- backend/src/services/youtube/api.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/backend/src/services/youtube/api.js b/backend/src/services/youtube/api.js index 039d292..bf222a0 100644 --- a/backend/src/services/youtube/api.js +++ b/backend/src/services/youtube/api.js @@ -3,6 +3,20 @@ import { formatDate, formatTime } from '../../utils/date.js'; const API_KEY = config.google.apiKey; const API_BASE = 'https://www.googleapis.com/youtube/v3'; +const FETCH_TIMEOUT = 10000; + +/** + * 타임아웃이 있는 fetch (업스트림 행으로 봇/요청이 무한 대기하는 것 방지) + */ +async function fetchWithTimeout(url, options = {}) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT); + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timeoutId); + } +} /** * ISO 8601 duration (PT1M30S) → 초 변환 @@ -31,7 +45,7 @@ function getVideoUrl(videoId, isShorts) { */ export async function getUploadsPlaylistId(channelId) { const url = `${API_BASE}/channels?part=contentDetails&id=${channelId}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (data.error) { @@ -52,7 +66,7 @@ export async function getChannelByHandle(handle) { // @ 제거 const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; const url = `${API_BASE}/channels?part=snippet,brandingSettings&forHandle=${cleanHandle}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (data.error) { @@ -84,7 +98,7 @@ export async function getChannelByHandle(handle) { */ export async function getChannelInfo(channelId) { const url = `${API_BASE}/channels?part=snippet,brandingSettings&id=${channelId}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (data.error) { @@ -115,7 +129,7 @@ export async function getChannelInfo(channelId) { */ async function getVideoDurations(videoIds) { const url = `${API_BASE}/videos?part=contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); const durations = {}; @@ -136,7 +150,7 @@ async function getVideoDurations(videoIds) { export async function fetchRecentVideoIds(channelId, maxResults = 10) { const fetchCount = Math.min(maxResults * 2, 50); const url = `${API_BASE}/activities?part=snippet,contentDetails&channelId=${channelId}&type=upload&maxResults=${fetchCount}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (data.error) { @@ -161,7 +175,7 @@ export async function fetchAllVideos(channelId, uploadsPlaylistId = null) { do { const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=50&key=${API_KEY}${pageToken ? `&pageToken=${pageToken}` : ''}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (data.error) { @@ -204,7 +218,7 @@ export async function fetchAllVideos(channelId, uploadsPlaylistId = null) { */ export async function fetchVideoInfo(videoId) { const url = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoId}&key=${API_KEY}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const data = await res.json(); if (!data.items?.length) {