groupware: add the Retry-After header in responses when the session cannot be retrieved

This commit is contained in:
Pascal Bleser
2025-10-20 16:34:47 +02:00
parent 8999caf5a1
commit f4ed735441
2 changed files with 51 additions and 25 deletions

View File

@@ -446,16 +446,17 @@ func (g *Groupware) ServeSSE(w http.ResponseWriter, r *http.Request) {
}
// Provide a JMAP Session for the
func (g *Groupware) session(user user, _ *log.Logger) (jmap.Session, bool, *GroupwareError) {
func (g *Groupware) session(user user, _ *log.Logger) (jmap.Session, bool, *GroupwareError, time.Time) {
s := g.sessionCache.Get(user.GetUsername())
if s != nil {
if s.Success() {
return s.Get(), true, nil
return s.Get(), true, nil, s.Until()
} else {
return jmap.Session{}, false, s.Error()
return jmap.Session{}, false, s.Error(), s.Until()
}
}
return jmap.Session{}, false, nil
// not sure this should happen:
return jmap.Session{}, false, nil, time.Time{}
}
func (g *Groupware) log(error *Error) {
@@ -491,18 +492,25 @@ func (g *Groupware) log(error *Error) {
l.Msg(error.Title)
}
func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Error) {
func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Error, retryAfter time.Time) {
if error == nil {
return
}
g.log(error)
w.Header().Add("Content-Type", ContentTypeJsonApi)
if !retryAfter.IsZero() {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
// either as an absolute timestamp:
// w.Header().Add("Retry-After", retryAfter.UTC().Format(time.RFC1123))
// or as a delay in seconds:
w.Header().Add("Retry-After", fmt.Sprintf("%.0f", time.Until(retryAfter).Seconds()))
}
render.Status(r, error.NumStatus)
w.WriteHeader(error.NumStatus)
render.Render(w, r, errorResponses(*error))
}
func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) (Response, bool) {
func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) (Response, time.Time, bool) {
ctx := r.Context()
sl := g.logger.SubloggerWithRequestID(ctx)
logger := &sl
@@ -510,24 +518,24 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler
user, err := g.userProvider.GetUser(r, ctx, logger)
if err != nil {
g.metrics.AuthenticationFailureCounter.Inc()
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication))
return Response{}, false
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication), time.Time{})
return Response{}, time.Time{}, false
}
if user == nil {
g.metrics.AuthenticationFailureCounter.Inc()
g.serveError(w, r, apiError(errorId(r, ctx), ErrorMissingAuthentication))
return Response{}, false
g.serveError(w, r, apiError(errorId(r, ctx), ErrorMissingAuthentication), time.Time{})
return Response{}, time.Time{}, false
}
logger = log.From(logger.With().Str(logUserId, log.SafeString(user.GetId())))
session, ok, gwerr := g.session(user, logger)
session, ok, gwerr, retryAfter := g.session(user, logger)
if gwerr != nil {
g.metrics.SessionFailureCounter.Inc()
errorId := errorId(r, ctx)
logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Str(logErrorId, errorId).Msg("failed to determine JMAP session")
g.serveError(w, r, apiError(errorId, *gwerr))
return Response{}, false
g.serveError(w, r, apiError(errorId, *gwerr), retryAfter)
return Response{}, retryAfter, false
}
if !ok {
// no session = authentication failed
@@ -535,8 +543,8 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler
errorId := errorId(r, ctx)
logger.Error().Str(logErrorId, errorId).Msg("could not authenticate, failed to find Session")
gwerr = &ErrorInvalidAuthentication
g.serveError(w, r, apiError(errorId, *gwerr))
return Response{}, false
g.serveError(w, r, apiError(errorId, *gwerr), retryAfter)
return Response{}, retryAfter, false
}
decoratedLogger := decorateLogger(logger, session)
@@ -550,7 +558,7 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler
}
response := handler(req)
return response, true
return response, time.Time{}, true
}
func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, response Response) {
@@ -606,7 +614,7 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons
}
func (g *Groupware) respond(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) {
response, ok := g.withSession(w, r, handler)
response, _, ok := g.withSession(w, r, handler)
if !ok {
return
}
@@ -620,21 +628,21 @@ func (g *Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(
user, err := g.userProvider.GetUser(r, ctx, logger)
if err != nil {
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication))
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication), time.Time{})
return
}
if user == nil {
g.serveError(w, r, apiError(errorId(r, ctx), ErrorMissingAuthentication))
g.serveError(w, r, apiError(errorId(r, ctx), ErrorMissingAuthentication), time.Time{})
return
}
logger = log.From(logger.With().Str(logUserId, log.SafeString(user.GetId())))
session, ok, gwerr := g.session(user, logger)
session, ok, gwerr, retryAfter := g.session(user, logger)
if gwerr != nil {
errorId := errorId(r, ctx)
logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Str(logErrorId, errorId).Msg("failed to determine JMAP session")
g.serveError(w, r, apiError(errorId, *gwerr))
g.serveError(w, r, apiError(errorId, *gwerr), retryAfter)
return
}
if !ok {
@@ -642,7 +650,7 @@ func (g *Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(
errorId := errorId(r, ctx)
logger.Error().Str(logErrorId, errorId).Msg("could not authenticate, failed to find Session")
gwerr = &ErrorInvalidAuthentication
g.serveError(w, r, apiError(errorId, *gwerr))
g.serveError(w, r, apiError(errorId, *gwerr), retryAfter)
return
}

View File

@@ -38,12 +38,16 @@ type cachedSession interface {
Error() *GroupwareError
// The timestamp of when this cached session information was obtained, regardless of success or failure.
Since() time.Time
// The timestamp of when this cached session information will be invalidated, regardless of success or failure.
Until() time.Time
}
// An implementation of a cachedSession that succeeded.
type succeededSession struct {
// Timestamp of when this succeededSession was created.
since time.Time
// Until when the session will be cached
until time.Time
// The JMAP Session itself.
session jmap.Session
}
@@ -62,11 +66,16 @@ func (s succeededSession) Error() *GroupwareError {
func (s succeededSession) Since() time.Time {
return s.since
}
func (s succeededSession) Until() time.Time {
return s.until
}
// An implementation of a cachedSession that failed.
type failedSession struct {
// Timestamp of when this failedSession was created.
since time.Time
// Until when the failure will be cached, without re-attempting to retrieve the Session.
until time.Time
// The error that caused the Session acquisition to fail.
err *GroupwareError
}
@@ -85,6 +94,9 @@ func (s failedSession) Error() *GroupwareError {
func (s failedSession) Since() time.Time {
return s.since
}
func (s failedSession) Until() time.Time {
return s.until
}
// Implements the ttlcache.Loader interface, by loading JMAP Sessions for users
// using the jmap.Client.
@@ -104,15 +116,21 @@ func (l *sessionCacheLoader) Load(c *ttlcache.Cache[sessionCacheKey, cachedSessi
sessionUrl, gwerr := l.sessionUrlProvider(username)
if gwerr != nil {
l.logger.Warn().Str("username", username).Str("code", gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
return c.Set(key, failedSession{since: time.Now(), err: gwerr}, l.errorTtl)
now := time.Now()
until := now.Add(l.errorTtl)
return c.Set(key, failedSession{since: now, until: until, err: gwerr}, l.errorTtl)
}
session, jerr := l.sessionSupplier(sessionUrl, username, l.logger)
if jerr != nil {
l.logger.Warn().Str("username", username).Err(jerr).Msgf("failed to create session for '%v'", key)
return c.Set(key, failedSession{since: time.Now(), err: groupwareErrorFromJmap(jerr)}, l.errorTtl)
now := time.Now()
until := now.Add(l.errorTtl)
return c.Set(key, failedSession{since: now, until: until, err: groupwareErrorFromJmap(jerr)}, l.errorTtl)
} else {
l.logger.Debug().Str("username", username).Msgf("successfully created session for '%v'", key)
return c.Set(key, succeededSession{since: time.Now(), session: session}, ttlcache.DefaultTTL) // use the TTL configured on the Cache
now := time.Now()
until := now.Add(ttlcache.DefaultTTL)
return c.Set(key, succeededSession{since: now, until: until, session: session}, ttlcache.DefaultTTL) // use the TTL configured on the Cache
}
}