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:
Oleks
2026-05-15 22:02:28 +03:00
parent 3c094d66fa
commit 47f3e4137e
3 changed files with 36 additions and 1 deletions
+1
View File
@@ -875,6 +875,7 @@ func Routes() *web.Router {
}))
}
m.AfterRouting(common.SessionTagMiddleware())
m.AfterRouting(context.APIContexter())
m.AfterRouting(checkDeprecatedAuthMethods)
+34
View File
@@ -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
View File
@@ -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()