| 75 | } |
| 76 | |
| 77 | func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, titlePlaintext, titleMarkdown, bodyPlaintext, bodyMarkdown, endpoint string) DeliveryFunc { |
| 78 | return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) { |
| 79 | // Prepare payload. |
| 80 | payload := WebhookPayload{ |
| 81 | Version: "1.1", |
| 82 | MsgID: msgID, |
| 83 | Title: titlePlaintext, |
| 84 | TitleMarkdown: titleMarkdown, |
| 85 | Body: bodyPlaintext, |
| 86 | BodyMarkdown: bodyMarkdown, |
| 87 | Payload: msgPayload, |
| 88 | } |
| 89 | m, err := json.Marshal(payload) |
| 90 | if err != nil { |
| 91 | return false, xerrors.Errorf("marshal payload: %v", err) |
| 92 | } |
| 93 | |
| 94 | // Prepare request. |
| 95 | // Outer context has a deadline (see CODER_NOTIFICATIONS_DISPATCH_TIMEOUT). |
| 96 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(m)) |
| 97 | if err != nil { |
| 98 | return false, xerrors.Errorf("create HTTP request: %v", err) |
| 99 | } |
| 100 | req.Header.Set("Content-Type", "application/json") |
| 101 | req.Header.Set("X-Message-Id", msgID.String()) |
| 102 | |
| 103 | // Send request. |
| 104 | resp, err := w.cl.Do(req) |
| 105 | if err != nil { |
| 106 | if errors.Is(err, context.DeadlineExceeded) { |
| 107 | return true, xerrors.Errorf("request timeout: %w", err) |
| 108 | } |
| 109 | |
| 110 | return true, xerrors.Errorf("request failed: %w", err) |
| 111 | } |
| 112 | defer resp.Body.Close() |
| 113 | |
| 114 | // Handle response. |
| 115 | if resp.StatusCode/100 > 2 { |
| 116 | // Body could be quite long here, let's grab the first 512B and hope it contains useful debug info. |
| 117 | respBody := make([]byte, 512) |
| 118 | lr := io.LimitReader(resp.Body, int64(len(respBody))) |
| 119 | n, err := lr.Read(respBody) |
| 120 | if err != nil && !errors.Is(err, io.EOF) { |
| 121 | return true, xerrors.Errorf("non-2xx response (%d), read body: %w", resp.StatusCode, err) |
| 122 | } |
| 123 | w.log.Warn(ctx, "unsuccessful delivery", slog.F("status_code", resp.StatusCode), |
| 124 | slog.F("response", string(respBody[:n])), slog.F("msg_id", msgID)) |
| 125 | return true, xerrors.Errorf("non-2xx response (%d)", resp.StatusCode) |
| 126 | } |
| 127 | |
| 128 | return false, nil |
| 129 | } |
| 130 | } |