diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index 33334257d4..23e00800d1 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -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 } diff --git a/services/groupware/pkg/groupware/groupware_session.go b/services/groupware/pkg/groupware/groupware_session.go index a85748f55b..9b1f0eab9b 100644 --- a/services/groupware/pkg/groupware/groupware_session.go +++ b/services/groupware/pkg/groupware/groupware_session.go @@ -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 } }