From 75199d4dcda4df223fd6ae0b7a740c9be2ff6fe8 Mon Sep 17 00:00:00 2001 From: Shinei Nouzen <157171073+Shineii86@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:59:17 +0530 Subject: [PATCH] fix: add cookie-aware session establishment for stream AJAX endpoints Root cause: upstream /ajax/server and /ajax/server/list endpoints require session cookies set by visiting the watch page. fetchWithMirror() didn't persist cookies between requests, causing upstream to return 500. - Extract cookies from set-cookie headers when fetching watch/home page - Pass cookies with AJAX requests for stream info and server list - Accept optional slug query param in /api/stream for targeted session - Both extractors now use cookie-aware axios for session establishment --- CHANGELOG.md | 10 +-- src/controllers/streamInfo.controller.js | 4 +- src/extractors/streamInfo.extractor.js | 82 ++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3808d..731a902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.2.2] - 2026-08-14 ### Fixed -- **CRITICAL**: Fixed `/api/stream` returning `null` URL when upstream API returns error responses (e.g. `{status: 500, result: "Bad request"}`) — extractor now detects upstream error status codes and throws meaningful errors instead of silently returning null -- Fixed `streamInfo.extractor.js` `extractStreamInfo` to detect upstream API error responses (`data.status >= 400`) and throw descriptive errors -- Fixed `streamInfo.extractor.js` `extractStreamInfo` to detect non-JSON error strings from upstream API (e.g. "Bad request") and throw errors instead of silently swallowing them -- Fixed `streamInfo.extractor.js` `extractServerList` to detect upstream API error responses and throw descriptive errors +- **CRITICAL**: Fixed `/api/stream` returning `null` URL — root cause was AJAX endpoints (`/ajax/server?get=`, `/ajax/server/list?servers=`) require session cookies from the watch page; `fetchWithMirror` didn't persist cookies between requests, causing upstream to return `{status:500, result:"Bad request"}` +- Fixed `streamInfo.extractor.js` `extractStreamInfo` to establish session with cookies before AJAX requests — fetches watch page or home page to capture `set-cookie` headers and passes them with the AJAX request +- Fixed `streamInfo.extractor.js` `extractServerList` to establish session with cookies before AJAX requests (same cookie-aware pattern) +- Fixed `streamInfo.controller.js` `getStreamInfo` to accept optional `slug` query parameter and pass it to extractor for session establishment - Fixed `streamInfo.controller.js` `getStreamInfo` to return HTTP 502 (Bad Gateway) with error message for upstream API failures instead of silently returning null URL -- Fixed `streamInfo.controller.js` `getServerList` to properly propagate upstream errors via error handler +- Fixed upstream error detection in both extractors — detects `status >= 400` and non-JSON error strings from upstream API ## [2.2.1] - 2026-08-12 diff --git a/src/controllers/streamInfo.controller.js b/src/controllers/streamInfo.controller.js index e43dafa..f13d895 100644 --- a/src/controllers/streamInfo.controller.js +++ b/src/controllers/streamInfo.controller.js @@ -44,7 +44,7 @@ import { getCache, setCache, TTL } from "../helper/cache.helper.js"; */ const getStreamInfo = async (req, res, next) => { try { - const { id } = req.query; + const { id, slug } = req.query; if (!id) { return res.status(400).json({ success: false, message: "Link ID is required" }); } @@ -55,7 +55,7 @@ const getStreamInfo = async (req, res, next) => { } let data; try { - data = await extractStreamInfo(id); + data = await extractStreamInfo(id, slug); } catch (upstreamError) { return res.status(502).json({ success: false, message: `Upstream API error: ${upstreamError.message}` }); } diff --git a/src/extractors/streamInfo.extractor.js b/src/extractors/streamInfo.extractor.js index aeb2207..01611ae 100644 --- a/src/extractors/streamInfo.extractor.js +++ b/src/extractors/streamInfo.extractor.js @@ -16,10 +16,11 @@ * ======= • ======= • ======= • ======= • =======• ======= */ +import axios from "axios"; import * as cheerio from "cheerio"; import { headers } from "../configs/header.config.js"; import { BASE_URL } from "../configs/dataUrl.js"; -import { fetchWithMirror } from "../helper/mirror.helper.js"; +import { fetchWithMirror, getWorkingMirror } from "../helper/mirror.helper.js"; import { normalizeServerName } from "./streamResolver.extractor.js"; // ══════════════════════════════════════════════════════════════ @@ -43,14 +44,48 @@ import { normalizeServerName } from "./streamResolver.extractor.js"; */ const extractStreamInfo = async (linkId, watchSlug = null) => { try { - // NOTE: Establish session by visiting watch page first (required for AJAX endpoints) + // NOTE: Establish session with cookies — AJAX endpoints require session cookies + const mirror = await getWorkingMirror(); + let cookieHeader = ""; + if (watchSlug) { - await fetchWithMirror(`/watch/${watchSlug}`); + try { + const sessionRes = await axios.get(`${mirror}/watch/${watchSlug}`, { + headers, + timeout: 10000, + maxRedirects: 5, + }); + const setCookies = sessionRes.headers["set-cookie"]; + if (Array.isArray(setCookies)) { + cookieHeader = setCookies.map(c => c.split(";")[0]).join("; "); + } + } catch { /* session establishment is best-effort */ } + } + + // NOTE: Fallback — if no slug provided, try fetching any page to get session cookies + if (!cookieHeader) { + try { + const fallbackRes = await axios.get(`${mirror}/home`, { + headers, + timeout: 10000, + maxRedirects: 5, + }); + const setCookies = fallbackRes.headers["set-cookie"]; + if (Array.isArray(setCookies)) { + cookieHeader = setCookies.map(c => c.split(";")[0]).join("; "); + } + } catch { /* fallback is best-effort */ } } const path = `/ajax/server?get=${linkId}`; + const requestHeaders = { + ...headers, + "X-Requested-With": "XMLHttpRequest", + ...(cookieHeader ? { "Cookie": cookieHeader } : {}), + }; + const { data: raw } = await fetchWithMirror(path, { - headers: { "X-Requested-With": "XMLHttpRequest" } + headers: requestHeaders }); // NOTE: Handle both string JSON and parsed object responses @@ -116,14 +151,47 @@ const extractStreamInfo = async (linkId, watchSlug = null) => { */ const extractServerList = async (episodeIds, watchSlug = null) => { try { - // NOTE: Establish session by visiting watch page first (required for AJAX endpoints) + // NOTE: Establish session with cookies — AJAX endpoints require session cookies + const mirror = await getWorkingMirror(); + let cookieHeader = ""; + if (watchSlug) { - await fetchWithMirror(`/watch/${watchSlug}`); + try { + const sessionRes = await axios.get(`${mirror}/watch/${watchSlug}`, { + headers, + timeout: 10000, + maxRedirects: 5, + }); + const setCookies = sessionRes.headers["set-cookie"]; + if (Array.isArray(setCookies)) { + cookieHeader = setCookies.map(c => c.split(";")[0]).join("; "); + } + } catch { /* session establishment is best-effort */ } + } + + if (!cookieHeader) { + try { + const fallbackRes = await axios.get(`${mirror}/home`, { + headers, + timeout: 10000, + maxRedirects: 5, + }); + const setCookies = fallbackRes.headers["set-cookie"]; + if (Array.isArray(setCookies)) { + cookieHeader = setCookies.map(c => c.split(";")[0]).join("; "); + } + } catch { /* fallback is best-effort */ } } const path = `/ajax/server/list?servers=${episodeIds}`; + const requestHeaders = { + ...headers, + "X-Requested-With": "XMLHttpRequest", + ...(cookieHeader ? { "Cookie": cookieHeader } : {}), + }; + const { data: raw } = await fetchWithMirror(path, { - headers: { "X-Requested-With": "XMLHttpRequest" } + headers: requestHeaders }); // NOTE: Parse JSON response first — fetchWithMirror returns raw text by default