fix: detect provider-level failures in generic webhook notifications (#2352)#2356
Conversation
…etarcaneapp#2352) Providers like PushPlus always return HTTP 200 even when the notification fails (e.g. wrong field name, invalid token). shoutrrr only inspects the HTTP status code, so the UI incorrectly reported "sent" while nothing was delivered. Add an optional SuccessBodyContains field to GenericConfig. When set, SendGenericWithTitle bypasses shoutrrr and makes the HTTP call directly, capturing the full response body. If the body does not contain the expected substring the send is reported as an error. Example PushPlus config: set successBodyContains to "\"code\":200" to distinguish a real success from the {"code":900,...} failure body.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| // sendGenericDirect makes the webhook HTTP call directly, giving access to the | ||
| // response body so that provider-level success/failure can be detected even | ||
| // when the HTTP status is always 200. | ||
| func sendGenericDirect(ctx context.Context, config models.GenericConfig, title, message string) error { |
There was a problem hiding this comment.
Unexported functions missing "Internal" suffix
Both new unexported functions — sendGenericDirect (line 161) and resolveWebhookURL (line 231) — are missing the required Internal suffix for private helpers.
| func sendGenericDirect(ctx context.Context, config models.GenericConfig, title, message string) error { | |
| func sendGenericDirectInternal(ctx context.Context, config models.GenericConfig, title, message string) error { |
Rule Used: What: All unexported functions must have the "Inte... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 161
Comment:
**Unexported functions missing "Internal" suffix**
Both new unexported functions — `sendGenericDirect` (line 161) and `resolveWebhookURL` (line 231) — are missing the required `Internal` suffix for private helpers.
```suggestion
func sendGenericDirectInternal(ctx context.Context, config models.GenericConfig, title, message string) error {
```
**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| // resolveWebhookURL parses and normalises the configured webhook URL, | ||
| // adding a default scheme when the user omitted one. | ||
| func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) { |
There was a problem hiding this comment.
"Internal" suffix required for unexported helper
Same naming rule applies here — the private helper should be resolveWebhookURLInternal.
| func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) { | |
| func resolveWebhookURLInternal(config models.GenericConfig) (*url.URL, error) { |
Rule Used: What: All unexported functions must have the "Inte... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 231
Comment:
**"Internal" suffix required for unexported helper**
Same naming rule applies here — the private helper should be `resolveWebhookURLInternal`.
```suggestion
func resolveWebhookURLInternal(config models.GenericConfig) (*url.URL, error) {
```
**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| // resolveWebhookURL parses and normalises the configured webhook URL, | ||
| // adding a default scheme when the user omitted one. | ||
| func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) { | ||
| parsed, err := url.Parse(config.WebhookURL) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid webhook URL: %w", err) | ||
| } | ||
|
|
||
| hasScheme := strings.Contains(config.WebhookURL, "://") | ||
| if parsed.Host == "" && !hasScheme { | ||
| scheme := "https" | ||
| if config.DisableTLS { | ||
| scheme = "http" | ||
| } | ||
| normalized := strings.TrimPrefix(config.WebhookURL, "//") | ||
| parsed, err = url.Parse(fmt.Sprintf("%s://%s", scheme, normalized)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid webhook URL: %w", err) | ||
| } | ||
| } | ||
|
|
||
| if parsed.Host == "" { | ||
| return nil, fmt.Errorf("invalid webhook URL: missing host") | ||
| } | ||
|
|
||
| switch strings.ToLower(parsed.Scheme) { | ||
| case "http", "https": | ||
| default: | ||
| return nil, fmt.Errorf("invalid webhook URL scheme: %s", parsed.Scheme) | ||
| } | ||
|
|
||
| return parsed, nil | ||
| } |
There was a problem hiding this comment.
URL-parsing logic duplicated from
BuildGenericURL
resolveWebhookURL reproduces the same scheme-normalisation and host-validation logic that already exists in BuildGenericURL (lines 25–45). If the normalisation rules ever diverge, the two code paths will behave differently for the same WebhookURL value. Consider extracting the shared logic into one helper and calling it from both BuildGenericURL and sendGenericDirect.
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 229-261
Comment:
**URL-parsing logic duplicated from `BuildGenericURL`**
`resolveWebhookURL` reproduces the same scheme-normalisation and host-validation logic that already exists in `BuildGenericURL` (lines 25–45). If the normalisation rules ever diverge, the two code paths will behave differently for the same `WebhookURL` value. Consider extracting the shared logic into one helper and calling it from both `BuildGenericURL` and `sendGenericDirect`.
How can I resolve this? If you propose a fix, please make it concise.
Summary
successBodyContainsfield toGenericConfig. When set,SendGenericWithTitlebypasses shoutrrr and makes the HTTP call directly, reads the full response body, and returns an error if the expected substring is absent.successBodyContains— they still go through shoutrrr.Example PushPlus config: set
successBodyContainsto"code":200to distinguish a real success (body contains{"code":200,...}) from a failure (body contains{"code":900,...}).Closes #2352
Test plan
successBodyContains: '"code":200'successBodyContains) — verify existing behaviour is unchanged🤖 Generated with Claude Code
Disclaimer Greptiles Reviews use AI, make sure to check over its work.
To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike
To have Greptile Re-Review the changes, mention
greptileai.Greptile Summary
This PR adds an optional
successBodyContainsfield toGenericConfigand a new direct HTTP path inSendGenericWithTitleto handle providers (e.g. PushPlus) that always return HTTP 200 but signal failure in the response body. Existing behaviour is fully preserved when the field is not set.Two new unexported helpers (
sendGenericDirect,resolveWebhookURL) are missing the project-requiredInternalsuffix, andresolveWebhookURLduplicates URL-normalisation logic already present inBuildGenericURL.Confidence Score: 5/5
Safe to merge; all findings are P2 style/naming issues that do not affect runtime behaviour.
No P0 or P1 issues found. The logic is sound, backward-compatible, and the two remaining findings are naming-convention violations and a code-duplication note.
backend/pkg/utils/notifications/generic_sender.go — naming convention violations on the two new private helpers.
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix: detect provider-level failures in g..." | Re-trigger Greptile
Context used: