Fix play proxy cancel crashing the server on client disconnect
This commit is contained in:
@@ -34,6 +34,11 @@ import { tmpdir } from 'node:os';
|
|||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { initDb, upsertUser, recordVideoAccess, getUserData, createProfile, getProfile, saveProfile, createSharedPlaylist, getSharedPlaylist } from './db.js';
|
import { initDb, upsertUser, recordVideoAccess, getUserData, createProfile, getProfile, saveProfile, createSharedPlaylist, getSharedPlaylist } from './db.js';
|
||||||
|
|
||||||
|
// A media proxy must not die because one client's stream hit an edge case
|
||||||
|
// (see /api/play cancel()): log and keep serving instead of crash-looping.
|
||||||
|
process.on('uncaughtException', (err) => console.error('[ytplayer] uncaught exception:', err));
|
||||||
|
process.on('unhandledRejection', (err) => console.error('[ytplayer] unhandled rejection:', err));
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||||
const APP_VERSION = process.env.APP_VERSION || '1.0.0';
|
const APP_VERSION = process.env.APP_VERSION || '1.0.0';
|
||||||
const YTDLP = process.env.YTDLP_PATH || 'yt-dlp';
|
const YTDLP = process.env.YTDLP_PATH || 'yt-dlp';
|
||||||
@@ -597,6 +602,13 @@ app.get('/api/play', async (c) => {
|
|||||||
const length = end - start + 1;
|
const length = end - start + 1;
|
||||||
|
|
||||||
const fmt = attempt.fmt;
|
const fmt = attempt.fmt;
|
||||||
|
// Client-disconnect handling: the active upstream body is LOCKED by its
|
||||||
|
// reader, so `body.cancel()` on it throws ("Cannot cancel a locked
|
||||||
|
// ReadableStream") — and an exception thrown from cancel() took the whole
|
||||||
|
// Bun process down on every seek / quality switch / aborted fetch. Cancel
|
||||||
|
// through the reader instead, and stop the slice loop via a flag.
|
||||||
|
let aborted = false;
|
||||||
|
let reader = null;
|
||||||
const body = new ReadableStream({
|
const body = new ReadableStream({
|
||||||
async start(ctrl) {
|
async start(ctrl) {
|
||||||
// Upstream body is a slice already; the slice boundaries below are
|
// Upstream body is a slice already; the slice boundaries below are
|
||||||
@@ -604,15 +616,15 @@ app.get('/api/play', async (c) => {
|
|||||||
let pos = start;
|
let pos = start;
|
||||||
let res = up;
|
let res = up;
|
||||||
try {
|
try {
|
||||||
while (pos <= end) {
|
while (pos <= end && !aborted) {
|
||||||
const sliceEnd = Math.min(end, pos + PLAY_CHUNK - 1);
|
const sliceEnd = Math.min(end, pos + PLAY_CHUNK - 1);
|
||||||
if (res === null) {
|
if (res === null) {
|
||||||
res = await fetch(fmt.url, { headers: { ...fmt.headers, Range: `bytes=${pos}-${sliceEnd}` }, redirect: 'follow' });
|
res = await fetch(fmt.url, { headers: { ...fmt.headers, Range: `bytes=${pos}-${sliceEnd}` }, redirect: 'follow' });
|
||||||
if (res.status !== 206 && res.status !== 200) throw new Error('upstream ' + res.status);
|
if (res.status !== 206 && res.status !== 200) throw new Error('upstream ' + res.status);
|
||||||
}
|
}
|
||||||
const reader = res.body.getReader();
|
reader = res.body.getReader();
|
||||||
let got = 0;
|
let got = 0;
|
||||||
while (got < sliceEnd - pos + 1) {
|
while (got < sliceEnd - pos + 1 && !aborted) {
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
const room = sliceEnd - pos + 1 - got;
|
const room = sliceEnd - pos + 1 - got;
|
||||||
@@ -620,17 +632,23 @@ app.get('/api/play', async (c) => {
|
|||||||
ctrl.enqueue(chunk);
|
ctrl.enqueue(chunk);
|
||||||
got += chunk.length;
|
got += chunk.length;
|
||||||
}
|
}
|
||||||
try { reader.cancel(); } catch {}
|
try { await reader.cancel(); } catch {}
|
||||||
|
reader = null;
|
||||||
|
if (aborted) break;
|
||||||
if (got === 0) throw new Error('upstream returned no bytes');
|
if (got === 0) throw new Error('upstream returned no bytes');
|
||||||
pos += got;
|
pos += got;
|
||||||
res = null;
|
res = null;
|
||||||
}
|
}
|
||||||
ctrl.close();
|
if (!aborted) ctrl.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctrl.error(err);
|
if (!aborted) { try { ctrl.error(err); } catch {} }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel() { try { up.body?.cancel(); } catch {} },
|
cancel() {
|
||||||
|
aborted = true;
|
||||||
|
const r = reader;
|
||||||
|
if (r) r.cancel().catch(() => {});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
headers.set('Content-Length', String(length));
|
headers.set('Content-Length', String(length));
|
||||||
|
|||||||
Reference in New Issue
Block a user