fix(notifications): surface provider API error details in test failure messages
This commit is contained in:
@@ -92,7 +92,7 @@ func respondSanitizedProviderError(c *gin.Context, status int, code, category, m
|
||||
c.JSON(status, response)
|
||||
}
|
||||
|
||||
var providerStatusCodePattern = regexp.MustCompile(`provider returned status\s+(\d{3})`)
|
||||
var providerStatusCodePattern = regexp.MustCompile(`provider returned status\s+(\d{3})(?::\s*(.+))?`)
|
||||
|
||||
func classifyProviderTestFailure(err error) (code string, category string, message string) {
|
||||
if err == nil {
|
||||
@@ -107,14 +107,18 @@ func classifyProviderTestFailure(err error) (code string, category string, messa
|
||||
return "PROVIDER_TEST_URL_INVALID", "validation", "Provider URL is invalid or blocked. Verify the URL and try again"
|
||||
}
|
||||
|
||||
if statusMatch := providerStatusCodePattern.FindStringSubmatch(errText); len(statusMatch) == 2 {
|
||||
if statusMatch := providerStatusCodePattern.FindStringSubmatch(errText); len(statusMatch) >= 2 {
|
||||
hint := ""
|
||||
if len(statusMatch) >= 3 && strings.TrimSpace(statusMatch[2]) != "" {
|
||||
hint = ": " + strings.TrimSpace(statusMatch[2])
|
||||
}
|
||||
switch statusMatch[1] {
|
||||
case "401", "403":
|
||||
return "PROVIDER_TEST_AUTH_REJECTED", "dispatch", "Provider rejected authentication. Verify your credentials"
|
||||
case "404":
|
||||
return "PROVIDER_TEST_ENDPOINT_NOT_FOUND", "dispatch", "Provider endpoint was not found. Verify the provider URL path"
|
||||
default:
|
||||
return "PROVIDER_TEST_REMOTE_REJECTED", "dispatch", fmt.Sprintf("Provider rejected the test request (HTTP %s)", statusMatch[1])
|
||||
return "PROVIDER_TEST_REMOTE_REJECTED", "dispatch", fmt.Sprintf("Provider rejected the test request (HTTP %s)%s", statusMatch[1], hint)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -157,6 +158,9 @@ func (w *HTTPWrapper) Send(ctx context.Context, request HTTPWrapperRequest) (*HT
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
if hint := extractProviderErrorHint(body); hint != "" {
|
||||
return nil, fmt.Errorf("provider returned status %d: %s", resp.StatusCode, hint)
|
||||
}
|
||||
return nil, fmt.Errorf("provider returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -410,6 +414,34 @@ func shouldRetry(resp *http.Response, err error) bool {
|
||||
return resp.StatusCode >= http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// extractProviderErrorHint attempts to extract a short, human-readable error description
|
||||
// from a JSON error response body. Only well-known fields are extracted to avoid
|
||||
// accidentally surfacing sensitive or overlong content from arbitrary providers.
|
||||
func extractProviderErrorHint(body []byte) string {
|
||||
if len(body) == 0 {
|
||||
return ""
|
||||
}
|
||||
var errResp map[string]any
|
||||
if err := json.Unmarshal(body, &errResp); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range []string{"description", "message", "error", "error_description"} {
|
||||
v, ok := errResp[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok || strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
if len(s) > 100 {
|
||||
s = s[:100] + "..."
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readCappedResponseBody(body io.Reader) ([]byte, error) {
|
||||
limited := io.LimitReader(body, MaxNotifyResponseBodyBytes+1)
|
||||
content, err := io.ReadAll(limited)
|
||||
|
||||
Reference in New Issue
Block a user