DIYgod/RSSHub · error · ConfigNotFoundError
YouTube RSS is disabled due to the lack of <a…
Error message
YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>
What it means
ConfigNotFoundError thrown by the YouTube subscriptions handler. Unlike /c/ and /live, this route needs the full OAuth stack — config.youtube.key AND clientId AND clientSecret AND refreshToken — because it reads the authenticated user's own subscription feed via the YouTube Data API on their behalf. The guard checks all four and fails if any is missing.
Solutions
- Create OAuth credentials (Desktop app type) in Google Cloud Console and set YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET.
- Use the Google OAuth Playground (redirect URI https://developers.google.com/oauthplayground) with the youtube.readonly scope to obtain a YOUTUBE_REFRESH_TOKEN.
- Set YOUTUBE_KEY as well (the route needs both API key and OAuth).
- Restart RSSHub after setting all four env vars.
Defensive patterns
Strategy: validation
Validate before calling
import { config } from '@/config';
function requireYouTubeOAuth() {
const c = config.youtube;
const missing = [];
if (!c?.key) missing.push('YOUTUBE_KEY');
if (!c?.clientId) missing.push('YOUTUBE_CLIENT_ID');
if (!c?.clientSecret) missing.push('YOUTUBE_CLIENT_SECRET');
if (!c?.refreshToken) missing.push('YOUTUBE_REFRESH_TOKEN');
if (missing.length) {
throw new ConfigNotFoundError(`YouTube subscriptions needs OAuth config. Missing: ${missing.join(', ')}. Use redirect URI https://developers.google.com/oauthplayground with scope youtube.readonly.`);
}
} Prevention
- Create OAuth credentials (Desktop app) in Google Cloud and set YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET.
- Generate YOUTUBE_REFRESH_TOKEN via the OAuth Playground with youtube.readonly scope and the playground redirect URI.
- Also set YOUTUBE_KEY — subscriptions need both API key and OAuth.
- Restart RSSHub after setting all four env vars.
When it happens
Trigger: Any request to /youtube/subscriptions when at least one of YOUTUBE_KEY, YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_REFRESH_TOKEN is unset. The guard short-circuits before getSubscriptions is called.
Common situations: Operator set only YOUTUBE_KEY and assumed subscriptions would work; OAuth credentials not created in Google Cloud; refresh token never generated via the OAuth playground; env vars named inconsistently.
Related errors
- YouTube RSS is disabled due to the lack of <a…
- YouTube RSS is disabled due to the lack of <a…
- Cannot get access token since MangaDex client ID or secret…
- Cannot get refresh token since MangaDex username or…
- Failed to retrieve access token from MangaDex API.
AI-assisted analysis of DIYgod/RSSHub@bed535e087 (2026-08-12).
Data as JSON: /api/errors/f4a4cbaffe5e3e6e.
Report an issue: GitHub.
Appendix: source
Thrown at lib/routes/youtube/subscriptions.ts:50
description: '',
},
],
},
radar: [
{
source: ['www.youtube.com/feed/subscriptions', 'www.youtube.com/feed/channels'],
target: '/subscriptions',
},
],
name: 'Subscriptions',
maintainers: ['TonyRL'],
handler,
url: 'www.youtube.com/feed/subscriptions',
};
async function handler(ctx) {
if (!config.youtube || !config.youtube.key || !config.youtube.clientId || !config.youtube.clientSecret || !config.youtube.refreshToken) {
throw new ConfigNotFoundError('YouTube RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const embed = !ctx.req.param('embed');
const channelIds = (await getSubscriptions('snippet', cache)).data.items.map((item) => item.snippet.resourceId.channelId);
const playlistIds = await pMap(channelIds, async (channelId) => (await getChannelWithId(channelId, 'contentDetails', cache)).data.items?.[0].contentDetails.relatedPlaylists.uploads, { concurrency: 30 });
let items = await pMap(playlistIds.filter(Boolean), async (playlistId) => (await getPlaylistItems(playlistId, 'snippet', cache))?.data.items, { concurrency: 30 });
items = items.flat();
items = items
.filter((i) => i && !i.error && i.snippet.title !== 'Private video' && i.snippet.title !== 'Deleted video')
.map((item) => {
const snippet = item.snippet;
const videoId = snippet.resourceId.videoId;
const img = getThumbnail(snippet.thumbnails);
return {View on GitHub (pinned to bed535e087)