Files
1Panel/agent/utils/webhook_sender/response.go
T

181 lines
5.1 KiB
Go

package webhook_sender
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
type responseRejection struct {
statusCode int
reason string
}
func DiagnosticText(value string, input Request) string {
return captureResponse([]byte(value), input, preparedTarget{})
}
func (e *responseRejection) Error() string {
return fmt.Sprintf("webhook provider rejected response (HTTP %d)", e.statusCode)
}
func validateResponse(preset Preset, statusCode int, body []byte) error {
if preset == "" {
preset = PresetGeneric
}
switch preset {
case PresetGeneric, PresetDiscord, PresetTeams, PresetSlack:
default:
return errors.New("unsupported webhook preset")
}
reason, failed := businessResponseFailure(body)
if preset == PresetSlack && reason == "" {
switch value := strings.TrimSpace(string(body)); value {
case "invalid_payload", "no_text", "invalid_token", "no_service", "no_service_id",
"no_active_hooks", "channel_not_found", "channel_is_archived",
"action_prohibited", "posting_to_general_channel_denied",
"team_disabled", "user_not_found", "too_many_attachments", "rollup_error":
reason = value
}
}
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
return &responseRejection{statusCode: statusCode, reason: reason}
}
if preset == PresetSlack {
if statusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
return &responseRejection{statusCode: statusCode, reason: reason}
}
return nil
}
if failed {
return &responseRejection{statusCode: statusCode, reason: reason}
}
return nil
}
func businessResponseFailure(body []byte) (string, bool) {
var response map[string]json.RawMessage
if err := json.Unmarshal(body, &response); err != nil || response == nil {
return "", false
}
message := responseMessage(response)
if message == "" {
var errorText string
if json.Unmarshal(response["error"], &errorText) == nil {
message = strings.TrimSpace(errorText)
} else {
var nested map[string]json.RawMessage
if json.Unmarshal(response["error"], &nested) == nil {
message = responseMessage(nested)
}
}
}
for _, key := range []string{"errcode", "errCode", "error_code", "ErrorCode"} {
if raw, exists := response[key]; exists {
code, valid := responseCode(raw)
if !valid || !isSuccessResponseCode(code, false) {
return responseFailureReason(key, code, message), true
}
}
}
for _, key := range []string{"success", "ok"} {
var success bool
if raw, exists := response[key]; exists && string(raw) != "null" && json.Unmarshal(raw, &success) == nil && !success {
return responseFailureReason(key, "false", message), true
}
}
if raw, exists := response["error"]; exists {
var value any
if json.Unmarshal(raw, &value) == nil {
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) != "" {
return responseFailureReason("error", "", typed), true
}
case map[string]any:
if len(typed) != 0 {
return responseFailureReason("error", "", message), true
}
case bool:
if typed {
return responseFailureReason("error", "true", message), true
}
case float64:
if typed != 0 {
return responseFailureReason("error", strconv.FormatFloat(typed, 'g', -1, 64), message), true
}
}
}
}
if message != "" {
for _, key := range []string{"code", "Code", "StatusCode", "statusCode"} {
if raw, exists := response[key]; exists {
code, valid := responseCode(raw)
if !valid || !isSuccessResponseCode(code, true) {
return responseFailureReason(key, code, message), true
}
}
}
}
return message, false
}
func responseMessage(response map[string]json.RawMessage) string {
for _, key := range []string{"errmsg", "msg", "message", "description", "StatusMessage", "Message"} {
var message string
if json.Unmarshal(response[key], &message) == nil && strings.TrimSpace(message) != "" {
return strings.TrimSpace(message)
}
}
return ""
}
func responseCode(raw json.RawMessage) (string, bool) {
var code json.Number
if json.Unmarshal(raw, &code) == nil && code != "" {
return code.String(), true
}
var text string
if json.Unmarshal(raw, &text) == nil && strings.TrimSpace(text) != "" {
return strings.TrimSpace(text), true
}
return "", false
}
func isSuccessResponseCode(code string, allowHTTP bool) bool {
number, err := strconv.ParseFloat(code, 64)
if err == nil {
return number == 0 || allowHTTP && number >= 200 && number < 300
}
return allowHTTP && (strings.EqualFold(code, "ok") || strings.EqualFold(code, "success"))
}
func responseFailureReason(field, code, message string) string {
reason := field
if code != "" {
reason += "=" + code
}
if message != "" {
reason += ": " + message
}
return reason
}
func safeResponseError(err error, input Request, target preparedTarget) error {
var rejection *responseRejection
if !input.CaptureResponse || !errors.As(err, &rejection) || rejection.reason == "" {
return err
}
reason := captureResponse([]byte(rejection.reason), input, target)
reason = strings.Join(strings.Fields(reason), " ")
if reason == "" {
return err
}
return fmt.Errorf("%w: %s", rejection, truncateUTF8(reason, 512))
}