feat(project): session-tag propagation for own-tab event suppression
Adds a router middleware that extracts the X-Session-Tag header from each request and decorates the request context via project_events.WithSessionTag. Service- and model-layer publishers then read it back via project_events.SessionTagFromContext and attach it to outgoing CardMoved / CardLinked / CardUnlinked events. The originating browser tab compares the incoming session_tag to its own and skips the echo, avoiding double-application of the optimistic local update. Other tabs see no tag match and apply the event normally. Wired into both the web router chain (before Contexter so the base context inherits the tag) and the API router chain (before APIContexter for the same reason).
This commit is contained in:
@@ -875,6 +875,7 @@ func Routes() *web.Router {
|
||||
}))
|
||||
}
|
||||
|
||||
m.AfterRouting(common.SessionTagMiddleware())
|
||||
m.AfterRouting(context.APIContexter())
|
||||
m.AfterRouting(checkDeprecatedAuthMethods)
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/services/project_events"
|
||||
)
|
||||
|
||||
// SessionTagHeader is the HTTP header browser tabs use to broadcast a
|
||||
// per-page-load identifier with every mutation request. The server
|
||||
// echoes it back inside SSE event payloads so the originating tab
|
||||
// can suppress its own event after applying the optimistic update.
|
||||
const SessionTagHeader = "X-Session-Tag"
|
||||
|
||||
// SessionTagMiddleware decorates each incoming request's context with
|
||||
// the X-Session-Tag header value when present. Service- and model-
|
||||
// layer publishers read the value via project_events.SessionTagFromContext.
|
||||
//
|
||||
// Empty / missing headers are a no-op.
|
||||
func SessionTagMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tag := r.Header.Get(SessionTagHeader)
|
||||
if tag != "" {
|
||||
ctx := project_events.WithSessionTag(r.Context(), tag)
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -295,7 +295,7 @@ func Routes() *web.Router {
|
||||
routes.Get("/ssh_info", misc.SSHInfo)
|
||||
routes.Get("/api/healthz", healthcheck.Check)
|
||||
|
||||
mid = append(mid, common.MustInitSessioner(), context.Contexter())
|
||||
mid = append(mid, common.SessionTagMiddleware(), common.MustInitSessioner(), context.Contexter())
|
||||
|
||||
// Get user from session if logged in.
|
||||
webAuth := newWebAuthMiddleware()
|
||||
|
||||
Reference in New Issue
Block a user