fix error not shown on send message endpoint. Fix error sometimes shown as context cancelled instead of error due to early cancel before body was read

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-21 10:06:26 +01:00
parent 46a162c031
commit 52a563c9cb
3 changed files with 24 additions and 8 deletions

View File

@@ -870,8 +870,10 @@ func (c *Campaign) SendEmailByCampaignRecipientID(g *gin.Context) {
}
// send message (email or API depending on campaign template configuration)
err := c.CampaignService.SendEmailByCampaignRecipientID(g.Request.Context(), session, id)
// handle responses
if ok := c.handleErrors(g, err); !ok {
// handle responses - sending failures are expected (invalid recipient email etc)
// so return as bad request instead of internal server error
if err != nil {
c.Response.BadRequestMessage(g, err.Error())
return
}
c.Response.OK(g, gin.H{})

View File

@@ -752,8 +752,8 @@ func (a *APISender) sendRequest(
apiRequestBody *apiRequestBody,
) (*http.Response, func(), error) {
// prepare request
reqCtx, reqCancel := context.WithTimeout(ctx, 3*time.Second)
defer reqCancel()
reqCtx, reqCancel := context.WithTimeout(ctx, 10*time.Second)
// context must stay alive until response body is read
if apiRequestBody == nil {
apiRequestBody = bytes.NewBuffer([]byte{})
}
@@ -764,6 +764,7 @@ func (a *APISender) sendRequest(
apiRequestBody,
)
if err != nil {
reqCancel()
return nil, func() {}, errs.Wrap(err)
}
// TODO these headers should be enrished with template variables like {{.FirstName}} or etc
@@ -774,10 +775,16 @@ func (a *APISender) sendRequest(
a.Logger.Debugw("sending request", "URL", apiRequestURL.String())
resp, err := http.DefaultClient.Do(req)
if err != nil {
reqCancel()
return nil, func() {}, errs.Wrap(err)
}
// #nosec
return resp, func() { resp.Body.Close() }, nil
// return cleanup function that closes body and cancels context
// context must not be canceled until after response body is read
// otherwise io.ReadAll(resp.Body) can fail with "context canceled"
return resp, func() {
resp.Body.Close()
reqCancel()
}, nil
}
type apiRequestURL = bytes.Buffer

View File

@@ -2274,7 +2274,7 @@ func (c *Campaign) saveSendingResult(
if sendError != nil {
data, err = vo.NewOptionalString1MB(sendError.Error())
if err != nil {
return errs.Wrap(fmt.Errorf("failed to create data: %s", err))
return errs.Wrap(fmt.Errorf("failed to create reason: %s", err))
}
}
campaignID := campaignRecipient.CampaignID.MustGet()
@@ -3331,7 +3331,9 @@ func (c *Campaign) SendEmailByCampaignRecipientID(
// send the email using existing logic from sendCampaignMessages
err = c.sendSingleCampaignMessage(ctx, session, &campaignID, campaignRecipient)
if err != nil {
c.Logger.Errorw("failed to send campaign message", "error", err)
// the failure is already logged in the campaign event with reason
c.Logger.Warnw("failed to send campaign message", "reason", err)
// don't wrap error, return as-is so controller can handle it as bad request
return errs.Wrap(err)
}
@@ -3467,6 +3469,11 @@ func (c *Campaign) sendSingleCampaignMessage(
return errs.Wrap(saveErr)
}
// if there was a sending error, log it as info since it's expected (e.g., invalid recipient)
if err != nil {
c.Logger.Infow("campaign message delivery failed", "reason", err.Error())
}
return err
}