Skip to content

feat(webhook): Send updated entries to webhook on feed refresh#4235

Draft
igortheclaw wants to merge 2 commits into
miniflux:mainfrom
igortheclaw:fix/push-updated-entries-to-integrations
Draft

feat(webhook): Send updated entries to webhook on feed refresh#4235
igortheclaw wants to merge 2 commits into
miniflux:mainfrom
igortheclaw:fix/push-updated-entries-to-integrations

Conversation

@igortheclaw

Copy link
Copy Markdown

Description

When Miniflux refreshes a feed, existing entries whose content has changed (title, body, author, etc.) are updated in the database but the webhook integration is never invoked for them. Only newly-created entries trigger PushEntries.

This PR adds a PushUpdatedEntries function that fires the webhook with an updated_entries event type whenever RefreshFeedEntries writes changes to pre-existing entries, giving webhook consumers the opportunity to react to content updates.

Motivation

Webhook-based pipelines that enrich entry content (e.g. adding live stats to the title or body) lose their changes on every feed refresh cycle: Miniflux overwrites the enriched fields with the original RSS content, and without this fix the webhook has no chance to re-apply the enrichment.

Only the webhook is triggered for updated entries — notification channels (Telegram, Pushover, Ntfy, Apprise, Discord, Slack, Matrix) are intentionally excluded to avoid re-notifying users about entries they have already seen.

Changes

  • internal/storage/entry.goRefreshFeedEntries now returns two slices: newEntries (entries created for the first time, unchanged behaviour) and updatedEntries (pre-existing entries whose content was rewritten). The function has a single caller (RefreshFeed), which has been updated accordingly.

  • internal/integration/integration.go — New PushUpdatedEntries() function. Mirrors PushEntries for the webhook only; notification channels are intentionally skipped.

  • internal/integration/webhook/webhook.go — New constant UpdatedEntriesEventType = "updated_entries", WebhookUpdatedEntriesEvent struct, and SendUpdatedEntriesWebhookEvent() method. Consumers can use the event_type field (or the X-Miniflux-Event-Type header) to distinguish content-update events from new-entry events.

  • internal/reader/handler/handler.goRefreshFeed captures the new updatedEntries return value and spawns PushUpdatedEntries in a goroutine alongside the existing PushEntries call.

Testing

New unit tests added:

internal/integration/integration_test.go

  • TestPushUpdatedEntriesLogsWebhookAttempt — webhook is called when enabled
  • TestPushUpdatedEntriesSkipsNotificationIntegrations — Telegram/Ntfy/Pushover/Discord/Slack/Matrix/Apprise are not invoked
  • TestPushUpdatedEntriesNoEntriesIsNoop — exits immediately for empty entries

internal/integration/webhook/webhook_test.go (new file)

  • TestSendUpdatedEntriesWebhookEventType — JSON payload contains event_type: "updated_entries"
  • TestSendUpdatedEntriesWebhookEventTypeHeaderX-Miniflux-Event-Type header is set correctly
  • TestSendUpdatedEntriesWebhookNoEntriesIsNoop — no HTTP call for empty entries
  • TestSendNewEntriesWebhookEventType — companion test confirming existing new_entries path is unaffected

All tests pass:

ok  miniflux.app/v2/internal/integration         0.007s
ok  miniflux.app/v2/internal/integration/webhook 0.009s

Breaking Changes

None. RefreshFeedEntries is an internal function with a single caller that has been updated in this PR. The webhook payload structure for new_entries and save_entry events is unchanged. The new updated_entries event is additive.

Have you followed these guidelines?

When Miniflux refreshes a feed and existing entries have their content
updated (title, body, author, etc.), those changes are now forwarded to
the configured webhook via a new `updated_entries` event type.

Previously, `RefreshFeedEntries` only returned newly-created entries to
the caller, so `PushEntries` (and therefore the webhook) was never
invoked for existing entries that had changed.  This caused webhook-based
enrichment pipelines to lose their enriched content on every refresh
cycle, because Miniflux silently overwrote the enriched fields in the DB
but never gave the webhook a chance to re-apply the enrichment.

Changes:
- storage/entry.go: RefreshFeedEntries now returns two slices:
  `newEntries` (created for the first time) and `updatedEntries`
  (pre-existing entries whose content changed).
- integration/integration.go: new PushUpdatedEntries() function that
  notifies integrations about content-changed entries.  Notification
  channels (Telegram, Pushover, Ntfy, Apprise, Discord, Slack, …) are
  intentionally excluded to avoid re-notifying users about entries they
  have already seen; only the webhook is invoked.
- integration/webhook/webhook.go: new `updated_entries` event type,
  WebhookUpdatedEntriesEvent struct, and
  SendUpdatedEntriesWebhookEvent() method, allowing webhook consumers to
  distinguish between new-entry events and content-update events.
- reader/handler/handler.go: RefreshFeed now captures the updatedEntries
  slice and spawns PushUpdatedEntries in a goroutine alongside the
  existing PushEntries call.
Tests for PushUpdatedEntries (integration package):
  - TestPushUpdatedEntriesLogsWebhookAttempt: verifies the webhook is
    called when WebhookEnabled is true.
  - TestPushUpdatedEntriesSkipsNotificationIntegrations: verifies that
    Telegram, Ntfy, Pushover, Discord, Slack, Matrix and Apprise are not
    invoked — only the webhook is triggered for content updates.
  - TestPushUpdatedEntriesNoEntriesIsNoop: verifies early exit when the
    entries slice is empty.

Tests for SendUpdatedEntriesWebhookEvent (webhook package):
  - TestSendUpdatedEntriesWebhookEventType: verifies the JSON payload
    contains event_type = "updated_entries".
  - TestSendUpdatedEntriesWebhookEventTypeHeader: verifies the
    X-Miniflux-Event-Type request header is set correctly.
  - TestSendUpdatedEntriesWebhookNoEntriesIsNoop: verifies no HTTP call
    is made for empty entries.
  - TestSendNewEntriesWebhookEventType: companion test confirming the
    existing new_entries path is unaffected.
@igortheclaw igortheclaw changed the title integration: send updated entries to webhook on feed refresh feat(webhook): Send updated entries to webhook on feed refresh Apr 10, 2026
@fguillot

Copy link
Copy Markdown
Member

This pull-request is obviously vibe-coded. The main issues are:

  • The webhook will fire for every existing entry on every refresh, not just entries whose content actually changed
  • Heavy code duplication in webhook.go

@fguillot fguillot marked this pull request as draft April 22, 2026 01:37
@fguillot fguillot requested a review from Copilot April 22, 2026 01:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new webhook event (updated_entries) to notify webhook consumers when existing feed entries are updated during refresh, without re-triggering user-facing notification channels.

Changes:

  • Update Storage.RefreshFeedEntries to return both newly created entries and updated existing entries.
  • Add integration.PushUpdatedEntries to invoke webhook delivery for updated entries only.
  • Extend the webhook client with updated_entries payload/header support and add unit tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/storage/entry.go Returns updatedEntries alongside newEntries during refresh.
internal/reader/handler/handler.go Triggers PushUpdatedEntries alongside existing PushEntries calls.
internal/integration/integration.go Adds PushUpdatedEntries that targets only the webhook integration.
internal/integration/webhook/webhook.go Adds UpdatedEntriesEventType and webhook sending/payload struct for updated entries.
internal/integration/integration_test.go Adds tests ensuring PushUpdatedEntries only targets webhook + handles empty slices.
internal/integration/webhook/webhook_test.go New tests for updated_entries event type in JSON payload and header.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/storage/entry.go
Comment on lines 404 to +405
err = s.updateEntry(tx, entry)
if err == nil {

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatedEntries is appended for every existing entry whenever updateExistingEntries is true, but updateEntry() currently runs an unconditional UPDATE and returns nil even if the incoming feed data is identical. This makes the new updated_entries webhook fire on every refresh for all existing entries, contradicting the function comment/PR intent (“content changed”). Consider changing the UPDATE to only match when at least one tracked column IS DISTINCT FROM the incoming value (or compare against the stored row first), and only append to updatedEntries when a real change occurred.

Suggested change
err = s.updateEntry(tx, entry)
if err == nil {
var changed bool
changed, err = s.updateEntry(tx, entry)
if err == nil && changed {

Copilot uses AI. Check for mistakes.
Comment thread internal/storage/entry.go
Comment on lines 404 to +407
err = s.updateEntry(tx, entry)
if err == nil {
updatedEntries = append(updatedEntries, entry)
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entries appended to updatedEntries are the parsed feed entries; for existing rows updateEntry() only scans id and does not populate persisted fields like status, created_at, changed_at, share_code, or starred. Because these entries are later sent to the webhook, the payload will likely contain zero/empty values for those fields (e.g., blank status, zero timestamps, starred=false). To keep the webhook payload accurate, consider returning/scanning the needed columns in updateEntry() (UPDATE … RETURNING status, created_at, changed_at, starred, share_code, …) or fetching the full entry from the DB before enqueueing it.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +144
webhookEntries = append(webhookEntries, &WebhookEntry{
ID: entry.ID,
UserID: entry.UserID,
FeedID: entry.FeedID,
Status: entry.Status,
Hash: entry.Hash,
Title: entry.Title,
URL: entry.URL,
CommentsURL: entry.CommentsURL,
Date: entry.Date,
CreatedAt: entry.CreatedAt,
ChangedAt: entry.ChangedAt,
Content: entry.Content,
Author: entry.Author,
ShareCode: entry.ShareCode,
Starred: entry.Starred,
ReadingTime: entry.ReadingTime,
Enclosures: entry.Enclosures,
Tags: entry.Tags,
})

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendUpdatedEntriesWebhookEvent() builds the webhook payload using fields like Status, CreatedAt, ChangedAt, ShareCode, and Starred from the provided entries. For updated entries coming from feed parsing/refresh, these fields are typically unset (model.NewEntry leaves them as zero values), so the updated_entries payload can be misleading (blank status, zero timestamps, etc.). Consider ensuring the caller passes fully-hydrated entries from the DB (or adjust the storage refresh/update path to RETURNING/scan these columns) before constructing the webhook payload.

Copilot uses AI. Check for mistakes.
@fguillot fguillot removed the AI Slop label Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants