Skip to content

feat(notifications): add webhook:// notification target - #1623

Open
s0up4200 wants to merge 4 commits into
developfrom
feat/webhook-notification-target
Open

feat(notifications): add webhook:// notification target#1623
s0up4200 wants to merge 4 commits into
developfrom
feat/webhook-notification-target

Conversation

@s0up4200

@s0up4200 s0up4200 commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator

Sends a flat JSON payload with source_app: "qui" to a configurable HTTP endpoint. Reuses existing notifiarrapi sub-object builders for torrent, backup, cross-seed, dir scan, orphan scan, and automation event data.

Summary by CodeRabbit

  • New Features
    • Added webhook notification support, enabling users to configure custom HTTP/HTTPS endpoints to receive notification events with automatic payload formatting, URL validation, and timeout protection.

Sends a flat JSON payload with source_app: "qui" to a configurable
HTTP endpoint. Reuses existing notifiarrapi sub-object builders for
torrent, backup, cross-seed, dir scan, orphan scan, and automation
event data.
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The changes introduce webhook notification support to the notification service. The implementation includes URL validation for webhook schemes, payload construction from event data, and HTTP POST delivery to configured webhook endpoints. Service routing and dispatch logic are updated to handle webhooks alongside existing notification channels.

Changes

Cohort / File(s) Summary
Webhook Integration
internal/services/notifications/service.go, internal/services/notifications/webhook.go
Added webhook URL validation with parseWebhookURL, webhook sending via sendWebhook method, and payload construction with buildWebhookPayload. Updated service routing to branch on "webhook" scheme and treat webhooks as non-human-readable metrics.
Webhook Tests
internal/services/notifications/webhook_test.go
Comprehensive test suite covering URL parsing validation (success/error cases), webhook sending with HTTP server simulation, error handling for non-2xx responses, payload construction for multiple event types (TorrentAdded, CrossSeedAutomationSucceeded), and validation of optional sub-object omission.

Sequence Diagram

sequenceDiagram
    participant Service
    participant PayloadBuilder
    participant URLValidator
    participant WebhookTarget
    
    Service->>URLValidator: parseWebhookURL(config)
    URLValidator-->>Service: validated URL
    
    Service->>PayloadBuilder: buildWebhookPayload(event data)
    PayloadBuilder->>PayloadBuilder: extract event fields<br/>(Torrent, CrossSeed, etc.)
    PayloadBuilder-->>Service: webhookPayload struct
    
    Service->>Service: Marshal payload to JSON
    Service->>WebhookTarget: POST request<br/>(headers, body, timeout)
    WebhookTarget-->>Service: HTTP response
    
    alt Status 2xx
        Service->>Service: Drain response body
    else Status non-2xx
        Service->>Service: Error on status
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A webhook springs forth with elegance and grace,
Carrying events across the internet space,
JSON payloads bundled with care,
HTTP POST sends them everywhere,
Notifications now flow like wind through the air! 🌬️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(notifications): add webhook:// notification target' clearly and concisely summarizes the main change: introducing a new webhook notification target scheme. It directly relates to the primary purpose of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webhook-notification-target
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/services/notifications/webhook_test.go (1)

17-277: Great coverage; consider consolidating repeated scenarios into table-driven tests.

The test surface is strong, but several webhook payload/send cases repeat setup/assert patterns and would be easier to maintain as table-driven cases.

As per coding guidelines, "Use table-driven test cases in Go tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/services/notifications/webhook_test.go` around lines 17 - 277, Tests
repeat similar setups and assertions; refactor into table-driven tests to
improve maintainability by consolidating scenarios for parseWebhookURL,
sendWebhook, buildWebhookPayload and ValidateURL. Create parameterized test
tables (slices of structs) for TestParseWebhookURL,
TestSendWebhookSuccess/TestSendWebhookHTTPError/TestSendWebhookNoAuthHeaders,
and the various buildWebhookPayload cases that vary only by Event fields and
expected payload subobjects; iterate the table in a single t.Run subtest, reuse
common server/Service setup (Service{} and httptest.Server) and common
assertions (status, headers, payload fields) while keeping unique expectations
per case, and ensure each case still calls parseWebhookURL, svc.sendWebhook,
svc.buildWebhookPayload or ValidateURL as appropriate. Use the existing
identifiers (parseWebhookURL, Service.sendWebhook, buildWebhookPayload,
ValidateURL, and test names) so the refactor replaces duplicated code with table
entries and per-case assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/services/notifications/webhook.go`:
- Around line 85-87: The non-2xx response path currently ignores the error from
io.ReadAll; update the error handling in the webhook send routine so the call to
io.ReadAll(io.LimitReader(res.Body, 4096)) checks its returned error and
includes that error (and any successfully read body chunk) in the returned
fmt.Errorf message; in other words, capture (body, err) from io.ReadAll, and if
err != nil return or wrap fmt.Errorf("unexpected status: %d body-read-error: %v
partial-body: %s", res.StatusCode, err, strings.TrimSpace(string(body)))
otherwise return the existing unexpected-status error using the trimmed body.
Reference res.StatusCode, res.Body, io.ReadAll, io.LimitReader and
strings.TrimSpace when locating the change.

---

Nitpick comments:
In `@internal/services/notifications/webhook_test.go`:
- Around line 17-277: Tests repeat similar setups and assertions; refactor into
table-driven tests to improve maintainability by consolidating scenarios for
parseWebhookURL, sendWebhook, buildWebhookPayload and ValidateURL. Create
parameterized test tables (slices of structs) for TestParseWebhookURL,
TestSendWebhookSuccess/TestSendWebhookHTTPError/TestSendWebhookNoAuthHeaders,
and the various buildWebhookPayload cases that vary only by Event fields and
expected payload subobjects; iterate the table in a single t.Run subtest, reuse
common server/Service setup (Service{} and httptest.Server) and common
assertions (status, headers, payload fields) while keeping unique expectations
per case, and ensure each case still calls parseWebhookURL, svc.sendWebhook,
svc.buildWebhookPayload or ValidateURL as appropriate. Use the existing
identifiers (parseWebhookURL, Service.sendWebhook, buildWebhookPayload,
ValidateURL, and test names) so the refactor replaces duplicated code with table
entries and per-case assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 736703cd-b37f-4be7-9b0c-ac685068dc5a

📥 Commits

Reviewing files that changed from the base of the PR and between 38991d8 and 043d518.

📒 Files selected for processing (3)
  • internal/services/notifications/service.go
  • internal/services/notifications/webhook.go
  • internal/services/notifications/webhook_test.go

Comment on lines +85 to +87
if res.StatusCode < 200 || res.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
return fmt.Errorf("unexpected status: %d body: %s", res.StatusCode, strings.TrimSpace(string(body)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle response body read errors explicitly on non-2xx responses.

io.ReadAll errors are currently ignored, which can hide useful failure context when diagnosing webhook delivery issues.

💡 Suggested fix
 if res.StatusCode < 200 || res.StatusCode >= 300 {
-	body, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
-	return fmt.Errorf("unexpected status: %d body: %s", res.StatusCode, strings.TrimSpace(string(body)))
+	body, readErr := io.ReadAll(io.LimitReader(res.Body, 4096))
+	if readErr != nil {
+		return fmt.Errorf("unexpected status: %d (failed to read response body: %w)", res.StatusCode, readErr)
+	}
+	return fmt.Errorf("unexpected status: %d body: %s", res.StatusCode, strings.TrimSpace(string(body)))
 }

As per coding guidelines, "Prefer explicit error handling over silent failures in Go".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/services/notifications/webhook.go` around lines 85 - 87, The non-2xx
response path currently ignores the error from io.ReadAll; update the error
handling in the webhook send routine so the call to
io.ReadAll(io.LimitReader(res.Body, 4096)) checks its returned error and
includes that error (and any successfully read body chunk) in the returned
fmt.Errorf message; in other words, capture (body, err) from io.ReadAll, and if
err != nil return or wrap fmt.Errorf("unexpected status: %d body-read-error: %v
partial-body: %s", res.StatusCode, err, strings.TrimSpace(string(body)))
otherwise return the existing unexpected-status error using the trimmed body.
Reference res.StatusCode, res.Body, io.ReadAll, io.LimitReader and
strings.TrimSpace when locating the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant