executeHook performs the actual HTTP request for the webhook. It handles marshalling the payload, creating the request, and processing the response. Retries are handled by the queue system, so this function only performs a single attempt.
(ctx context.Context, hook *Hook, payload HookPayload)
| 50 | // It handles marshalling the payload, creating the request, and processing the response. |
| 51 | // Retries are handled by the queue system, so this function only performs a single attempt. |
| 52 | func (m *redisHookManager) executeHook(ctx context.Context, hook *Hook, payload HookPayload) error { |
| 53 | // Create HTTP client with timeout |
| 54 | client := &http.Client{ |
| 55 | Timeout: time.Duration(hook.Timeout) * time.Second, |
| 56 | } |
| 57 | |
| 58 | // Marshal payload with explicit handling |
| 59 | payloadBytes, err := json.Marshal(payload) |
| 60 | if err != nil { |
| 61 | return fmt.Errorf("failed to marshal payload: %w", err) |
| 62 | } |
| 63 | |
| 64 | // Validate JSON before sending |
| 65 | if !json.Valid(payloadBytes) { |
| 66 | return fmt.Errorf("invalid JSON payload generated") |
| 67 | } |
| 68 | |
| 69 | logrus.WithFields(logrus.Fields{ |
| 70 | "hook_id": hook.ID, |
| 71 | "hook_name": hook.Name, |
| 72 | "hook_url": hook.URL, |
| 73 | "hook_type": hook.Type, |
| 74 | }).Info("Executing webhook") |
| 75 | |
| 76 | timestamp := strconv.FormatInt(time.Now().Unix(), 10) |
| 77 | |
| 78 | if m.config == nil { |
| 79 | return fmt.Errorf("config is not initialized") |
| 80 | } |
| 81 | secret := m.config.Server.SecretKey |
| 82 | // Create signature: HMAC-SHA256(timestamp + "." + payload) |
| 83 | signatureData := timestamp + "." + string(payloadBytes) |
| 84 | mac := hmac.New(sha256.New, []byte(secret)) |
| 85 | mac.Write([]byte(signatureData)) |
| 86 | signature := hex.EncodeToString(mac.Sum(nil)) |
| 87 | |
| 88 | req, err := http.NewRequestWithContext(ctx, "POST", hook.URL, bytes.NewBuffer(payloadBytes)) |
| 89 | if err != nil { |
| 90 | return fmt.Errorf("failed to create request: %w", err) |
| 91 | } |
| 92 | |
| 93 | req.Header.Set("Content-Type", "application/json") |
| 94 | req.Header.Set("X-LedgerForge-Signature", signature) |
| 95 | req.Header.Set("X-LedgerForge-Timestamp", timestamp) |
| 96 | req.Header.Set("X-Hook-ID", hook.ID) |
| 97 | req.Header.Set("X-Hook-Type", string(hook.Type)) |
| 98 | |
| 99 | resp, err := client.Do(req) |
| 100 | if err != nil { |
| 101 | // Update hook execution status on failure |
| 102 | _ = m.updateHookStatus(ctx, hook, false) |
| 103 | if ctx.Err() != nil { |
| 104 | logrus.WithFields(logrus.Fields{ |
| 105 | "hook_id": hook.ID, |
| 106 | "hook_type": hook.Type, |
| 107 | "error": err, |
| 108 | }).Error("Hook execution cancelled due to context timeout") |
| 109 | return ctx.Err() |