diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 2a00dfeb..09d9fb82 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -5,6 +5,7 @@ import axios from 'axios'; import { TMDBService } from './tmdbService'; import { logger } from '../utils/logger'; import { getCatalogDisplayName } from '../utils/catalogNameUtils'; +import { createSafeAxiosConfig } from '../utils/axiosConfig'; // Add a constant for storing the data source preference const DATA_SOURCE_KEY = 'discover_data_source'; @@ -1476,7 +1477,7 @@ class CatalogService { logger.warn(`Search failed for catalog in ${addon.name}:`, s.reason); } } - + if (addonResults.length === 0) { logger.log(`No results from ${addon.name}`); return; @@ -1542,7 +1543,7 @@ class CatalogService { logger.warn(`Addon ${manifest.name} (${manifest.id}) has no URL, skipping search`); return []; } - + // Extract base URL and preserve query params (same logic as stremioService.getAddonBaseURL) const [baseUrlPart, queryParams] = chosenUrl.split('?'); let cleanBaseUrl = baseUrlPart.replace(/manifest\.json$/, '').replace(/\/$/, ''); @@ -1554,7 +1555,7 @@ class CatalogService { const encodedCatalogId = encodeURIComponent(catalogId); const encodedQuery = encodeURIComponent(query); - + // Try path-style URL first (per Stremio protocol) url = `${cleanBaseUrl}/catalog/${type}/${encodedCatalogId}/search=${encodedQuery}.json`; @@ -1566,9 +1567,7 @@ class CatalogService { logger.log(`Searching ${manifest.name} (${type}/${catalogId}):`, url); - const response = await axios.get<{ metas: any[] }>(url, { - timeout: 10000, // 10 second timeout per addon - }); + const response = await axios.get<{ metas: any[] }>(url, createSafeAxiosConfig(10000)); const metas = response.data?.metas || []; diff --git a/src/services/pluginService.ts b/src/services/pluginService.ts index f9f9236a..01f68509 100644 --- a/src/services/pluginService.ts +++ b/src/services/pluginService.ts @@ -5,6 +5,7 @@ import { logger } from '../utils/logger'; import { Stream } from '../types/streams'; import { cacheService } from './cacheService'; import CryptoJS from 'crypto-js'; +import { safeAxiosConfig, createSafeAxiosConfig } from '../utils/axiosConfig'; const MAX_CONCURRENT_SCRAPERS = 5; const MAX_INFLIGHT_KEYS = 30; @@ -407,13 +408,12 @@ class LocalScraperService { : `${repositoryUrl}/manifest.json`; const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`; - const response = await axios.get(manifestUrl, { - timeout: 10000, + const response = await axios.get(manifestUrl, createSafeAxiosConfig(10000, { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } - }); + })); if (response.data && response.data.name) { logger.log('[LocalScraperService] Found repository name in manifest:', response.data.name); @@ -645,14 +645,13 @@ class LocalScraperService { : `${this.repositoryUrl}/manifest.json`; const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`; - const response = await axios.get(manifestUrl, { - timeout: 10000, + const response = await axios.get(manifestUrl, createSafeAxiosConfig(10000, { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '0' } - }); + })); const manifest: ScraperManifest = response.data; // Store repository name from manifest @@ -740,14 +739,13 @@ class LocalScraperService { : `${repo.url}/manifest.json`; const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`; - const response = await axios.get(manifestUrl, { - timeout: 10000, + const response = await axios.get(manifestUrl, createSafeAxiosConfig(10000, { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '0' } - }); + })); const manifest: ScraperManifest = response.data; // Update repository name from manifest @@ -823,14 +821,13 @@ class LocalScraperService { // Add cache-busting parameters to force fresh download const scraperUrlWithCacheBust = `${scraperUrl}?t=${Date.now()}&v=${Math.random()}`; - const response = await axios.get(scraperUrlWithCacheBust, { - timeout: 15000, + const response = await axios.get(scraperUrlWithCacheBust, createSafeAxiosConfig(15000, { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '0' } - }); + })); const scraperCode = response.data; // Store scraper info and code diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 65d129c9..64ca6535 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -5,6 +5,7 @@ import EventEmitter from 'eventemitter3'; import { localScraperService } from './pluginService'; import { DEFAULT_SETTINGS, AppSettings } from '../hooks/useSettings'; import { TMDBService } from './tmdbService'; +import { safeAxiosConfig, createSafeAxiosConfig } from '../utils/axiosConfig'; // Create an event emitter for addon changes export const addonEmitter = new EventEmitter(); @@ -626,7 +627,7 @@ class StremioService { : `${url.replace(/\/$/, '')}/manifest.json`; const response = await this.retryRequest(async () => { - return await axios.get(manifestUrl); + return await axios.get(manifestUrl, safeAxiosConfig); }); const manifest = response.data; @@ -878,7 +879,7 @@ class StremioService { // For page 1 without filters, try simple URL first (best compatibility) if (pageSkip === 0 && extraParts.length === 0) { if (__DEV__) console.log(`🔍 [getCatalog] Trying simple URL for ${manifest.name}: ${urlSimple}`); - response = await this.retryRequest(async () => axios.get(urlSimple)); + response = await this.retryRequest(async () => axios.get(urlSimple, safeAxiosConfig)); // Check if we got valid metas - if empty, try other styles if (!response?.data?.metas || response.data.metas.length === 0) { throw new Error('Empty response from simple URL'); @@ -890,7 +891,7 @@ class StremioService { try { // Try path-style URL (correct per protocol) if (__DEV__) console.log(`🔍 [getCatalog] Trying path-style URL for ${manifest.name}: ${urlPathStyle}`); - response = await this.retryRequest(async () => axios.get(urlPathStyle)); + response = await this.retryRequest(async () => axios.get(urlPathStyle, safeAxiosConfig)); // Check if we got valid metas - if empty, try query-style if (!response?.data?.metas || response.data.metas.length === 0) { throw new Error('Empty response from path-style URL'); @@ -899,7 +900,7 @@ class StremioService { try { // Try legacy query-style URL as last resort if (__DEV__) console.log(`🔍 [getCatalog] Trying query-style URL for ${manifest.name}: ${urlQueryStyle}`); - response = await this.retryRequest(async () => axios.get(urlQueryStyle)); + response = await this.retryRequest(async () => axios.get(urlQueryStyle, safeAxiosConfig)); } catch (e3) { if (__DEV__) console.log(`❌ [getCatalog] All URL styles failed for ${manifest.name}`); throw e3; @@ -994,7 +995,7 @@ class StremioService { if (isSupported) { try { const response = await this.retryRequest(async () => { - return await axios.get(url, { timeout: 10000 }); + return await axios.get(url, createSafeAxiosConfig(10000)); }); @@ -1025,7 +1026,7 @@ class StremioService { const response = await this.retryRequest(async () => { - return await axios.get(url, { timeout: 10000 }); + return await axios.get(url, createSafeAxiosConfig(10000)); }); @@ -1096,7 +1097,7 @@ class StremioService { const response = await this.retryRequest(async () => { - return await axios.get(url, { timeout: 10000 }); + return await axios.get(url, createSafeAxiosConfig(10000)); }); @@ -1412,7 +1413,7 @@ class StremioService { logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`); const response = await this.retryRequest(async () => { - return await axios.get(url); + return await axios.get(url, safeAxiosConfig); }); let processedStreams: Stream[] = []; @@ -1462,13 +1463,12 @@ class StremioService { const response = await this.retryRequest(async () => { logger.log(`Making request to ${url} with timeout ${timeout}ms`); - return await axios.get(url, { - timeout, + return await axios.get(url, createSafeAxiosConfig(timeout, { headers: { 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' } - }); + })); }, 5); // Increase retries for stream fetching if (response.data && response.data.streams && Array.isArray(response.data.streams)) { @@ -1770,7 +1770,7 @@ class StremioService { : `${baseUrl}/subtitles/${type}/${encodedId}.json`; } logger.log(`[getSubtitles] Fetching subtitles from ${addon.name}: ${url}`); - const response = await this.retryRequest(async () => axios.get(url, { timeout: 10000 })); + const response = await this.retryRequest(async () => axios.get(url, createSafeAxiosConfig(10000))); if (response.data && Array.isArray(response.data.subtitles)) { logger.log(`[getSubtitles] Got ${response.data.subtitles.length} subtitles from ${addon.name}`); return response.data.subtitles.map((sub: any, index: number) => ({ @@ -1910,7 +1910,7 @@ class StremioService { const url = `${baseUrl}/addon_catalog/${type}/${encodeURIComponent(id)}.json${queryParams ? `?${queryParams}` : ''}`; logger.log(`[getAddonCatalogs] Fetching from ${addon.name}: ${url}`); - const response = await this.retryRequest(() => axios.get(url, { timeout: 10000 })); + const response = await this.retryRequest(() => axios.get(url, createSafeAxiosConfig(10000))); if (response.data?.addons && Array.isArray(response.data.addons)) { results.push(...response.data.addons); diff --git a/src/utils/axiosConfig.ts b/src/utils/axiosConfig.ts new file mode 100644 index 00000000..1558e5ce --- /dev/null +++ b/src/utils/axiosConfig.ts @@ -0,0 +1,26 @@ + +export const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB in bytes + +/** + * Default axios request configuration with response size limits. + * Apply this to all axios.get() calls to prevent OOM crashes. + */ +export const safeAxiosConfig = { + maxContentLength: MAX_RESPONSE_SIZE, + maxBodyLength: MAX_RESPONSE_SIZE, +}; + +/** + * Creates a safe axios config with response size limits and optional timeout. + * @param timeout - Optional timeout in milliseconds (default: 10000) + * @param additionalConfig - Additional axios config to merge + * @returns Axios request config with safety limits + */ +export const createSafeAxiosConfig = ( + timeout: number = 10000, + additionalConfig?: Record +) => ({ + ...safeAxiosConfig, + timeout, + ...additionalConfig, +});