Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fixing email sending broken when attempting to send emails from different goroutines due to a data race #494

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions base_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type options struct {

// Client is the Twilio SendGrid Go client
type Client struct {
rest.Request
apiKey string
emailOptions TwilioEmailOptions
}

func (o *options) baseURL() string {
Expand Down Expand Up @@ -57,19 +58,41 @@ func requestNew(options options) rest.Request {

// Send sends an email through Twilio SendGrid
func (cl *Client) Send(email *mail.SGMailV3) (*rest.Response, error) {
return cl.SendWithContext(context.Background(), email)
return cl.SendWithContext(context.Background(), email, nil)
}

// SendWithContext sends an email through Twilio SendGrid with context.Context.
func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3) (*rest.Response, error) {
cl.Body = mail.GetRequestBody(email)
// SendWithHeaders sends an email through Twilio SendGrid with additional headers
func (cl *Client) SendWithHeaders(email *mail.SGMailV3, headers map[string]string) (*rest.Response, error) {
return cl.SendWithContext(context.Background(), email, headers)
}

// SendWithContext sends an email through Twilio SendGrid with context.Context
func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3, headers map[string]string) (*rest.Response, error) {
var request rest.Request

if cl.apiKey != "" {
request = GetRequest(cl.apiKey, "/v3/mail/send", "")
} else if cl.emailOptions != (TwilioEmailOptions{}) {
request = GetTwilioEmailRequest(cl.emailOptions)
} else {
return nil, errors.New("no API key or email options provided")
}

request.Method = "POST"

// Add any custom headers provided by the caller.
for k, v := range headers {
request.Headers[k] = v
}

request.Body = mail.GetRequestBody(email)
// when Content-Encoding header is set to "gzip"
// mail body is compressed using gzip according to
// https://docs.sendgrid.com/api-reference/mail-send/mail-send#mail-body-compression
if cl.Headers["Content-Encoding"] == "gzip" {
if request.Headers["Content-Encoding"] == "gzip" {
var gzipped bytes.Buffer
gz := gzip.NewWriter(&gzipped)
if _, err := gz.Write(cl.Body); err != nil {
if _, err := gz.Write(request.Body); err != nil {
return nil, err
}
if err := gz.Flush(); err != nil {
Expand All @@ -79,9 +102,9 @@ func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3) (*r
return nil, err
}

cl.Body = gzipped.Bytes()
request.Body = gzipped.Bytes()
}
return MakeRequestWithContext(ctx, cl.Request)
return MakeRequestWithContext(ctx, request)
}

// DefaultClient is used if no custom HTTP client is defined
Expand Down
7 changes: 3 additions & 4 deletions sendgrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package sendgrid

import (
"errors"
"github.com/sendgrid/rest"
"net/url"

"github.com/sendgrid/rest"
)

// sendGridOptions for CreateRequest
Expand Down Expand Up @@ -51,9 +52,7 @@ func createSendGridRequest(sgOptions sendGridOptions) rest.Request {

// NewSendClient constructs a new Twilio SendGrid client given an API key
func NewSendClient(key string) *Client {
request := GetRequest(key, "/v3/mail/send", "")
request.Method = "POST"
return &Client{request}
return &Client{apiKey: key}
}

// extractEndpoint extracts the endpoint from a baseURL
Expand Down
54 changes: 49 additions & 5 deletions sendgrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -1640,10 +1641,52 @@ func Test_test_mail_batch__batch_id__get(t *testing.T) {
assert.Equal(t, 200, response.StatusCode, "Wrong status code returned")
}

func Test_test_client_send_is_thread_safe(t *testing.T) {
apiKey := "SENDGIRD_APIKEY"
const numRequests = 5
var wg sync.WaitGroup
client := NewSendClient(apiKey)

// Launch multiple goroutines to send concurrent requests
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
from := &mail.Email{
Name: "Name",
Address: "[email protected]",
}

to := &mail.Email{
Name: "Recipient",
Address: "[email protected]",
}

email := &mail.SGMailV3{
From: from,
Personalizations: []*mail.Personalization{
{
To: []*mail.Email{to},
Subject: "Subject",
},
},
Content: []*mail.Content{
{
Type: "text/plain",
Value: "Value",
},
},
}

client.Send(email)
}(i)
}
wg.Wait()
}

func Test_test_send_client_with_mail_body_compression_enabled(t *testing.T) {
apiKey := "SENDGRID_API_KEY"
client := NewSendClient(apiKey)
client.Headers["Content-Encoding"] = "gzip"

emailBytes := []byte(` {
"asm": {
Expand Down Expand Up @@ -1780,8 +1823,10 @@ func Test_test_send_client_with_mail_body_compression_enabled(t *testing.T) {
email := &mail.SGMailV3{}
err := json.Unmarshal(emailBytes, email)
assert.Nil(t, err, fmt.Sprintf("Unmarshal error: %v", err))
client.Request.Headers["X-Mock"] = "202"
response, err := client.Send(email)

headers := map[string]string{"Content-Encoding": "gzip", "X-Mock": "202"}

response, err := client.SendWithHeaders(email, headers)
if err != nil {
t.Log(err)
}
Expand Down Expand Up @@ -1929,8 +1974,7 @@ func Test_test_send_client(t *testing.T) {
email := &mail.SGMailV3{}
err := json.Unmarshal(emailBytes, email)
assert.Nil(t, err, fmt.Sprintf("Unmarshal error: %v", err))
client.Request.Headers["X-Mock"] = "202"
response, err := client.Send(email)
response, err := client.SendWithHeaders(email, map[string]string{"X-Mock": "202"})
if err != nil {
t.Log(err)
}
Expand Down
5 changes: 2 additions & 3 deletions twilio_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ type TwilioEmailOptions struct {

// NewTwilioEmailSendClient constructs a new Twilio Email client given a username and password
func NewTwilioEmailSendClient(username, password string) *Client {
request := GetTwilioEmailRequest(TwilioEmailOptions{Username: username, Password: password, Endpoint: "/v3/mail/send"})
request.Method = "POST"
return &Client{request}
emailOptions := &TwilioEmailOptions{Username: username, Password: password, Endpoint: "/v3/mail/send"}
return &Client{emailOptions: *emailOptions}
}

// GetTwilioEmailRequest create Request
Expand Down
5 changes: 3 additions & 2 deletions twilio_email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (

func TestNewTwilioEmailSendClient(t *testing.T) {
mailClient := NewTwilioEmailSendClient("username", "password")
assert.Equal(t, "https://email.twilio.com/v3/mail/send", mailClient.BaseURL)
assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", mailClient.Headers["Authorization"])
request := GetTwilioEmailRequest(mailClient.emailOptions)
assert.Equal(t, "https://email.twilio.com/v3/mail/send", request.BaseURL)
assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", request.Headers["Authorization"])
}

func TestGetTwilioEmailRequest(t *testing.T) {
Expand Down
Loading