odysseus-dev/odysseus · error · Error

ntfy reminder was not sent

Error message

ntfy reminder was not sent

What it means

Delivery-level failure for ntfy test reminders. The POST returned OK, channel is 'ntfy', but data.ntfy_sent is falsy. Message is data.ntfy_error when present, otherwise this literal string.

Source

Thrown at static/js/settings.js:2710

            channel: channelSel.value,
            // Mirror the in-UI AI Synthesis toggle + persona so the test never
            // races a pending save and lets the user preview changes before
            // hitting Save.
            llm_synthesis: !!(llmToggle && llmToggle.checked),
            llm_persona: (personaSel && personaSel.value) || '',
            ...(channelSel.value === 'webhook' ? {
              webhook_integration_id: webhookIntgSel?.value || '',
              webhook_payload_template: webhookTemplateIn?.value.trim() || '',
            } : {}),
          }),
        });
        const data = await res.json();
        if (!res.ok) throw new Error(data.detail || 'Server error');
        if (channelSel.value === 'email' && !data.email_sent) {
          throw new Error(data.email_error || 'Email reminder was not sent');
        }
        if (channelSel.value === 'ntfy' && !data.ntfy_sent) {
          throw new Error(data.ntfy_error || 'ntfy reminder was not sent');
        }
        if (channelSel.value === 'webhook' && !data.webhook_sent) {
          const activeChannel = data.channel ? ` (server used channel: "${data.channel}")` : '';
          throw new Error((data.webhook_error || 'Webhook reminder was not sent') + activeChannel);
        }
        let status = 'Delivered via ' + channelSel.value;
        if (data.synthesis) status += ' (AI: "' + data.synthesis.slice(0, 60) + '...")';
        if (data.email_sent) status += ' — email sent';
        if (data.ntfy_sent) status += ' — ntfy sent';
        if (data.webhook_sent) status += ' — webhook sent';
        if (testMsg) { testMsg.textContent = status; testMsg.style.color = 'var(--green, #50fa7b)'; }
        // Also fire a browser notification so user can see it
        if ('Notification' in window && Notification.permission === 'granted') {
          try {
            new Notification('Test Reminder', {
              body: data.synthesis || 'This is a test reminder.',
              tag: 'reminder-test',
              icon: '/static/favicon.ico',

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Inspect data.ntfy_error in the response for the exact upstream failure
  2. Verify the ntfy server URL/topic configured on the backend
  3. Curl the ntfy endpoint from the server host to confirm egress
  4. Add a fuller fallback message so missing ntfy_error still points at config

Example fix

// before
        if (channelSel.value === 'ntfy' && !data.ntfy_sent) {
          throw new Error(data.ntfy_error || 'ntfy reminder was not sent');
        }

// after
        if (channelSel.value === 'ntfy' && !data.ntfy_sent) {
          throw new Error(data.ntfy_error || 'ntfy reminder was not sent (verify ntfy URL/topic)');
        }
Defensive patterns

Strategy: try-catch

Validate before calling

// client can only pre-check channel selection; ntfy reachability is server-side
if (channelSel.value !== 'ntfy') return;

Try / catch

if (channelSel.value === 'ntfy' && !data.ntfy_sent) { throw new Error(data.ntfy_error || 'ntfy reminder was not sent (verify ntfy URL/topic)'); }

Prevention

When it happens

Trigger: POST test returns 200 with {ntfy_sent: false} — ntfy server URL wrong or unreachable, topic rejected, rate-limited, or the server's HTTP POST to ntfy threw and was captured into ntfy_error.

Common situations: Self-hosted ntfy instance down or DNS stale; ntfy.sh rate limits; typo'd topic name; server egress blocked; ntfy_error absent so the generic message hides the cause.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/f7944f8febceff14. Report an issue: GitHub.