fix(anime): retry cold upstream fetches in the stream strip proxy

Right after an episode resolves, the bridge (or the host behind it) can
error on the very first segment fetches and be fine a moment later; mpv
sweeps the playlist of failing segments in ~2s and gives up with "no
audio or video data played" while a manual reload of the same URL plays.
Retry a failed GET once after 400ms and log upstream error statuses so
the next failure names the real cause in the app log.
This commit is contained in:
2026-08-01 00:24:26 -07:00
parent 835426d1e9
commit df4c20e971
3 changed files with 117 additions and 3 deletions
+49 -2
View File
@@ -68,8 +68,12 @@ export interface StreamStripProxyOptions {
/** Read per request so a bridge restart on a new port keeps working. */
upstreamOrigin: () => string;
log?: (message: string) => void;
/** Pause before the single retry of a failed upstream GET. */
retryDelayMs?: number;
}
const DEFAULT_RETRY_DELAY_MS = 400;
export interface StreamStripProxyHandle {
origin: string;
port: number;
@@ -97,6 +101,7 @@ export function startStreamStripProxy(
options: StreamStripProxyOptions,
): Promise<StreamStripProxyHandle> {
const log = options.log ?? (() => {});
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
const server = http.createServer((req, res) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
@@ -114,19 +119,61 @@ export function startStreamStripProxy(
const requestHeaders = forwardableHeaders(req.headers);
delete requestHeaders.host;
res.on('error', () => {});
requestUpstream(req, res, upstreamUrl, requestHeaders, 0);
});
/**
* One delayed retry on a failed GET: right after an episode resolve, the
* bridge (or the host behind it) can error on the very first segment
* fetches and be fine a moment later — mpv treats a playlist full of failed
* segments as a dead file and gives up for good.
*/
function requestUpstream(
req: http.IncomingMessage,
res: http.ServerResponse,
upstreamUrl: URL,
requestHeaders: http.OutgoingHttpHeaders,
attempt: number,
): void {
const mayRetry = req.method === 'GET' && attempt === 0;
const retry = (): void => {
setTimeout(
() => requestUpstream(req, res, upstreamUrl, requestHeaders, attempt + 1),
retryDelayMs,
);
};
const upstreamRequest = http.request(
upstreamUrl,
{ method: req.method, headers: requestHeaders },
(upstream) => handleUpstreamResponse(req, res, upstream),
(upstream) => {
const status = upstream.statusCode ?? 502;
if (status === 404 || status >= 500) {
if (mayRetry) {
log(`[stream-proxy] upstream ${status} for ${upstreamUrl.pathname}; retrying once`);
upstream.resume();
retry();
return;
}
log(`[stream-proxy] upstream ${status} for ${upstreamUrl.pathname}`);
}
handleUpstreamResponse(req, res, upstream);
},
);
upstreamRequest.on('error', (error) => {
if (mayRetry) {
log(`[stream-proxy] upstream request failed: ${String(error)}; retrying once`);
retry();
return;
}
log(`[stream-proxy] upstream request failed: ${String(error)}`);
if (!res.headersSent) res.writeHead(502);
res.end();
});
upstreamRequest.end();
});
}
function handleUpstreamResponse(
req: http.IncomingMessage,