fix(webhooks): Use SONARR_INSTANCES/RADARR_INSTANCES config for notification routes
All checks were successful
Build and Push Docker Image / build (push) Successful in 31s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m36s

The notification routes were using process.env.SONARR_URL directly,
which is undefined when using the newer SONARR_INSTANCES JSON format.

Changes:
- Added getFirstSonarrInstance() and getFirstRadarrInstance() helpers
- Updated /notifications, /notifications/test, and /notifications/sofarr-webhook
  routes to use instance config from getSonarrInstances()/getRadarrInstances()
- Returns 503 error if no instances are configured

Fixes: 'Invalid URL' errors when calling Sonarr/Radarr notification APIs
This commit is contained in:
2026-05-19 20:42:59 +01:00
parent af58e1bf2a
commit 9fd60bcfed
2 changed files with 64 additions and 22 deletions

View File

@@ -4,7 +4,16 @@ const axios = require('axios');
const router = express.Router(); const router = express.Router();
const requireAuth = require('../middleware/requireAuth'); const requireAuth = require('../middleware/requireAuth');
const sanitizeError = require('../utils/sanitizeError'); const sanitizeError = require('../utils/sanitizeError');
const { getWebhookSecret, getSofarrBaseUrl } = require('../utils/config'); const { getWebhookSecret, getSofarrBaseUrl, getRadarrInstances } = require('../utils/config');
// Helper to get first Radarr instance (for notification proxy routes)
function getFirstRadarrInstance() {
const instances = getRadarrInstances();
if (!instances || instances.length === 0) {
return null;
}
return instances[0];
}
router.use(requireAuth); router.use(requireAuth);
@@ -60,9 +69,13 @@ router.get('/movies', async (req, res) => {
// Notification proxy routes (Phase 3) // Notification proxy routes (Phase 3)
// GET /api/radarr/notifications - list all notifications // GET /api/radarr/notifications - list all notifications
router.get('/notifications', async (req, res) => { router.get('/notifications', async (req, res) => {
const instance = getFirstRadarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Radarr not configured' });
}
try { try {
const response = await axios.get(`${process.env.RADARR_URL}/api/v3/notification`, { const response = await axios.get(`${instance.url}/api/v3/notification`, {
headers: { 'X-Api-Key': process.env.RADARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
res.json(response.data); res.json(response.data);
} catch (error) { } catch (error) {
@@ -121,9 +134,13 @@ router.delete('/notifications/:id', async (req, res) => {
// POST /api/radarr/notifications/test - test notification // POST /api/radarr/notifications/test - test notification
router.post('/notifications/test', async (req, res) => { router.post('/notifications/test', async (req, res) => {
const instance = getFirstRadarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Radarr not configured' });
}
try { try {
const response = await axios.post(`${process.env.RADARR_URL}/api/v3/notification/test`, req.body, { const response = await axios.post(`${instance.url}/api/v3/notification/test`, req.body, {
headers: { 'X-Api-Key': process.env.RADARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
res.json(response.data); res.json(response.data);
} catch (error) { } catch (error) {
@@ -150,6 +167,10 @@ router.get('/notifications/schema', async (req, res) => {
// POST /api/radarr/notifications/sofarr-webhook - one-click Sofarr webhook setup // POST /api/radarr/notifications/sofarr-webhook - one-click Sofarr webhook setup
router.post('/notifications/sofarr-webhook', async (req, res) => { router.post('/notifications/sofarr-webhook', async (req, res) => {
const instance = getFirstRadarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Radarr not configured' });
}
try { try {
const sofarrBaseUrl = getSofarrBaseUrl(); const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret(); const webhookSecret = getWebhookSecret();
@@ -164,8 +185,8 @@ router.post('/notifications/sofarr-webhook', async (req, res) => {
const webhookUrl = `${sofarrBaseUrl}/api/webhook/radarr`; const webhookUrl = `${sofarrBaseUrl}/api/webhook/radarr`;
// Check if Sofarr webhook already exists // Check if Sofarr webhook already exists
const listResponse = await axios.get(`${process.env.RADARR_URL}/api/v3/notification`, { const listResponse = await axios.get(`${instance.url}/api/v3/notification`, {
headers: { 'X-Api-Key': process.env.RADARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
const existingNotification = listResponse.data.find(n => n.name === 'Sofarr'); const existingNotification = listResponse.data.find(n => n.name === 'Sofarr');
@@ -190,17 +211,17 @@ router.post('/notifications/sofarr-webhook', async (req, res) => {
if (existingNotification) { if (existingNotification) {
// Update existing notification // Update existing notification
const response = await axios.put( const response = await axios.put(
`${process.env.RADARR_URL}/api/v3/notification/${existingNotification.id}`, `${instance.url}/api/v3/notification/${existingNotification.id}`,
{ ...notificationPayload, id: existingNotification.id }, { ...notificationPayload, id: existingNotification.id },
{ headers: { 'X-Api-Key': process.env.RADARR_API_KEY } } { headers: { 'X-Api-Key': instance.apiKey } }
); );
res.json(response.data); res.json(response.data);
} else { } else {
// Create new notification // Create new notification
const response = await axios.post( const response = await axios.post(
`${process.env.RADARR_URL}/api/v3/notification`, `${instance.url}/api/v3/notification`,
notificationPayload, notificationPayload,
{ headers: { 'X-Api-Key': process.env.RADARR_API_KEY } } { headers: { 'X-Api-Key': instance.apiKey } }
); );
res.json(response.data); res.json(response.data);
} }

View File

@@ -4,7 +4,16 @@ const axios = require('axios');
const router = express.Router(); const router = express.Router();
const requireAuth = require('../middleware/requireAuth'); const requireAuth = require('../middleware/requireAuth');
const sanitizeError = require('../utils/sanitizeError'); const sanitizeError = require('../utils/sanitizeError');
const { getWebhookSecret, getSofarrBaseUrl } = require('../utils/config'); const { getWebhookSecret, getSofarrBaseUrl, getSonarrInstances } = require('../utils/config');
// Helper to get first Sonarr instance (for notification proxy routes)
function getFirstSonarrInstance() {
const instances = getSonarrInstances();
if (!instances || instances.length === 0) {
return null;
}
return instances[0];
}
router.use(requireAuth); router.use(requireAuth);
@@ -60,9 +69,13 @@ router.get('/series', async (req, res) => {
// Notification proxy routes (Phase 3) // Notification proxy routes (Phase 3)
// GET /api/sonarr/notifications - list all notifications // GET /api/sonarr/notifications - list all notifications
router.get('/notifications', async (req, res) => { router.get('/notifications', async (req, res) => {
const instance = getFirstSonarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Sonarr not configured' });
}
try { try {
const response = await axios.get(`${process.env.SONARR_URL}/api/v3/notification`, { const response = await axios.get(`${instance.url}/api/v3/notification`, {
headers: { 'X-Api-Key': process.env.SONARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
res.json(response.data); res.json(response.data);
} catch (error) { } catch (error) {
@@ -121,9 +134,13 @@ router.delete('/notifications/:id', async (req, res) => {
// POST /api/sonarr/notifications/test - test notification // POST /api/sonarr/notifications/test - test notification
router.post('/notifications/test', async (req, res) => { router.post('/notifications/test', async (req, res) => {
const instance = getFirstSonarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Sonarr not configured' });
}
try { try {
const response = await axios.post(`${process.env.SONARR_URL}/api/v3/notification/test`, req.body, { const response = await axios.post(`${instance.url}/api/v3/notification/test`, req.body, {
headers: { 'X-Api-Key': process.env.SONARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
res.json(response.data); res.json(response.data);
} catch (error) { } catch (error) {
@@ -150,6 +167,10 @@ router.get('/notifications/schema', async (req, res) => {
// POST /api/sonarr/notifications/sofarr-webhook - one-click Sofarr webhook setup // POST /api/sonarr/notifications/sofarr-webhook - one-click Sofarr webhook setup
router.post('/notifications/sofarr-webhook', async (req, res) => { router.post('/notifications/sofarr-webhook', async (req, res) => {
const instance = getFirstSonarrInstance();
if (!instance) {
return res.status(503).json({ error: 'Sonarr not configured' });
}
try { try {
const sofarrBaseUrl = getSofarrBaseUrl(); const sofarrBaseUrl = getSofarrBaseUrl();
const webhookSecret = getWebhookSecret(); const webhookSecret = getWebhookSecret();
@@ -164,8 +185,8 @@ router.post('/notifications/sofarr-webhook', async (req, res) => {
const webhookUrl = `${sofarrBaseUrl}/api/webhook/sonarr`; const webhookUrl = `${sofarrBaseUrl}/api/webhook/sonarr`;
// Check if Sofarr webhook already exists // Check if Sofarr webhook already exists
const listResponse = await axios.get(`${process.env.SONARR_URL}/api/v3/notification`, { const listResponse = await axios.get(`${instance.url}/api/v3/notification`, {
headers: { 'X-Api-Key': process.env.SONARR_API_KEY } headers: { 'X-Api-Key': instance.apiKey }
}); });
const existingNotification = listResponse.data.find(n => n.name === 'Sofarr'); const existingNotification = listResponse.data.find(n => n.name === 'Sofarr');
@@ -190,17 +211,17 @@ router.post('/notifications/sofarr-webhook', async (req, res) => {
if (existingNotification) { if (existingNotification) {
// Update existing notification // Update existing notification
const response = await axios.put( const response = await axios.put(
`${process.env.SONARR_URL}/api/v3/notification/${existingNotification.id}`, `${instance.url}/api/v3/notification/${existingNotification.id}`,
{ ...notificationPayload, id: existingNotification.id }, { ...notificationPayload, id: existingNotification.id },
{ headers: { 'X-Api-Key': process.env.SONARR_API_KEY } } { headers: { 'X-Api-Key': instance.apiKey } }
); );
res.json(response.data); res.json(response.data);
} else { } else {
// Create new notification // Create new notification
const response = await axios.post( const response = await axios.post(
`${process.env.SONARR_URL}/api/v3/notification`, `${instance.url}/api/v3/notification`,
notificationPayload, notificationPayload,
{ headers: { 'X-Api-Key': process.env.SONARR_API_KEY } } { headers: { 'X-Api-Key': instance.apiKey } }
); );
res.json(response.data); res.json(response.data);
} }