MM-27507: Propagate rate limit errors to client (#15230)

* MM-27507: Propagate rate limit errors to client

We return an error from SendInviteEmails instead of just logging it
to let the client know that a rate limit error has happened.

The status code is chosen as 413 (entity too large) instead of 429 (too many requests)
because it's not the request which is rate limited, but the payload inside it which is.

Ideally, the email sending should have been implemented by a queue which would just return
an error to the client when full. That is also why we are not returning an X-Retry-After
and X-Reset-After in the headers because that would mix with the actual rate limiting.

A separate header X-Email-Invite-Reset-After might do the job, but it comes at an extra cost
of additional API surface and a clunky API. Instead, that information is contained in the error
response. The web client needs to just surface the error. An API client will have to do
a bit more work to parse the error if it needs to automatically know when to retry. Given that
an email sending client is not a very common use case, we decide to keep the API clean.

This decision can be revisited if it becomes problematic in the future.

https://mattermost.atlassian.net/browse/MM-27507

* Fixing translations

* Added retry_after and reset_after in API response.
Этот коммит содержится в:
Agniva De Sarker
2020-08-13 22:19:05 +05:30
коммит произвёл GitHub
родитель 20e44399c7
Коммит 11513a8d0d
7 изменённых файлов: 137 добавлений и 31 удалений

Просмотреть файл

@@ -115,7 +115,11 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
}
auditRec.AddMeta("errors", errList)
if len(goodEmails) > 0 {
c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = err
return
}
}
// in graceful mode we return both the successful ones and the failed ones
w.Write([]byte(model.EmailInviteWithErrorToJson(invitesWithErrors)))
@@ -132,7 +136,11 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": s}, "", http.StatusBadRequest)
return
}
c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL)
err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
auditRec.Success()

Просмотреть файл

@@ -2833,6 +2833,25 @@ func TestInviteUsersToTeam(t *testing.T) {
require.NotNil(t, invitesWithErrors[0].Error)
require.Nil(t, invitesWithErrors[1].Error)
}, "override restricted domains")
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
th.BasicTeam.AllowedDomains = "common.com"
_, err := th.App.UpdateTeam(th.BasicTeam)
require.Nilf(t, err, "%v, Should update the team", err)
emailList := make([]string, 22)
for i := 0; i < 22; i++ {
emailList[i] = "test-" + strconv.Itoa(i) + "@common.com"
}
okMsg, resp := client.InviteUsersToTeam(th.BasicTeam.Id, emailList)
require.False(t, okMsg, "should return false")
CheckRequestEntityTooLargeStatus(t, resp)
CheckErrorMessage(t, resp, "app.email.rate_limit_exceeded.app_error")
_, resp = client.InviteUsersToTeamGracefully(th.BasicTeam.Id, emailList)
CheckRequestEntityTooLargeStatus(t, resp)
CheckErrorMessage(t, resp, "app.email.rate_limit_exceeded.app_error")
}, "rate limits")
}
func TestInviteGuestsToTeam(t *testing.T) {
@@ -2942,6 +2961,32 @@ func TestInviteGuestsToTeam(t *testing.T) {
err := th.App.InviteNewUsersToTeam([]string{"user@global.com"}, th.BasicTeam.Id, th.BasicUser.Id)
require.Nil(t, err, "non guest user invites should not be affected by the guest domain restrictions")
})
t.Run("rate limit", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.RestrictCreationToDomains = "@guest.com" })
_, err := th.App.UpdateTeam(th.BasicTeam)
require.Nilf(t, err, "%v, Should update the team", err)
emailList := make([]string, 22)
for i := 0; i < 22; i++ {
emailList[i] = "test-" + strconv.Itoa(i) + "@guest.com"
}
invite := &model.GuestsInvite{
Emails: emailList,
Channels: []string{th.BasicChannel.Id},
Message: "test message",
}
err = th.App.InviteGuestsToChannels(th.BasicTeam.Id, invite, th.BasicUser.Id)
require.NotNil(t, err)
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
_, err = th.App.InviteGuestsToChannelsGracefully(th.BasicTeam.Id, invite, th.BasicUser.Id)
require.NotNil(t, err)
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
})
}
func TestGetTeamInviteInfo(t *testing.T) {