Summary
The plugin's buffered responseWriter merges its buffered headers into the real writer by assignment when WriteHeader is called. When Traefik's sticky-session load balancer writes its cookie before proxying the request (the writer is not ready yet, so the cookie lands in the buffer), the merge overwrites the Set-Cookie headers of the backend response. Only the sticky cookie reaches the client; every cookie set by the backend on that response is silently dropped.
Why this matters (use case)
Any OAuth/OIDC login flow behind a sticky Traefik service breaks: the backend's redirect to the IdP sets a state cookie (e.g. WildFly/Elytron's OAuth_Token_Request_State) on the same response where Traefik sets its sticky cookie. The state cookie is dropped, so the IdP callback fails — WildFly logs WARN [org.wildfly.security.http.oidc] No state cookie and returns a plain 400 Bad Request.
The bug only fires on requests where Traefik actually writes the sticky cookie (i.e. the client doesn't send it yet): fresh browsers, private windows, expired session cookies. Returning users with the sticky cookie already set are unaffected, which makes it look like random user-specific failures and very hard to diagnose. We hit this in production on ~80 environments (Traefik v3.6.11, plugin v1.3.0).
Current behavior (main.go, responseWriter.WriteHeader)
headers := r.responseWriter.Header()
for header, value := range r.headers {
headers[header] = value
}
Timeline of a request through sablier (dynamic) → service with sticky cookie:
- The plugin wraps the writer;
ready=false, so Header() returns the buffered map.
- Traefik's load balancer calls
WriteStickyCookie before ServeHTTP on the proxy → the sticky cookie is Added to the buffered map.
- The proxy sends the request to the backend; the
httptrace WroteHeaders/WroteRequest callbacks flip ready=true.
- The proxy copies the backend response headers (including its
Set-Cookie) onto the real writer (Header() now returns it).
- The proxy calls
WriteHeader → the merge loop assigns headers["Set-Cookie"] = ["<sticky>"], replacing the backend's cookies.
Reproduction
Traefik (tested v3.6.11) + this plugin as a local plugin, file provider: a router with the sablier middleware (dynamic strategy, Sablier answering X-Sablier-Session-Status: ready) in front of a service with sticky.cookie enabled, backend answering 302 + Set-Cookie: TESTSTATE=abc123.
curl -sD - http://proxy/ (no sticky cookie sent) → response contains only the sticky cookie; TESTSTATE is gone.
- Same request with the sticky cookie already set →
TESTSTATE passes through (Traefik doesn't rewrite the sticky cookie).
- Same router without the sablier middleware → both cookies present (Traefik core is fine, the drop happens in the plugin's merge).
Proposed fix
Set-Cookie is multi-valued — each value is an independent cookie, so merging by assignment is always lossy. Append the buffered values instead of overwriting for this header (other headers keep the current overwrite semantics):
if header == "Set-Cookie" {
headers[header] = append(headers[header], value...)
} else {
headers[header] = value
}
PR with the fix + regression tests (unit-level on responseWriter and end-to-end through ServeHTTP with an httptrace-firing next handler): #58
Summary
The plugin's buffered
responseWritermerges its buffered headers into the real writer by assignment whenWriteHeaderis called. When Traefik's sticky-session load balancer writes its cookie before proxying the request (the writer is notreadyyet, so the cookie lands in the buffer), the merge overwrites theSet-Cookieheaders of the backend response. Only the sticky cookie reaches the client; every cookie set by the backend on that response is silently dropped.Why this matters (use case)
Any OAuth/OIDC login flow behind a sticky Traefik service breaks: the backend's redirect to the IdP sets a state cookie (e.g. WildFly/Elytron's
OAuth_Token_Request_State) on the same response where Traefik sets its sticky cookie. The state cookie is dropped, so the IdP callback fails — WildFly logsWARN [org.wildfly.security.http.oidc] No state cookieand returns a plain400 Bad Request.The bug only fires on requests where Traefik actually writes the sticky cookie (i.e. the client doesn't send it yet): fresh browsers, private windows, expired session cookies. Returning users with the sticky cookie already set are unaffected, which makes it look like random user-specific failures and very hard to diagnose. We hit this in production on ~80 environments (Traefik v3.6.11, plugin v1.3.0).
Current behavior (
main.go,responseWriter.WriteHeader)Timeline of a request through
sablier (dynamic) → service with sticky cookie:ready=false, soHeader()returns the buffered map.WriteStickyCookiebeforeServeHTTPon the proxy → the sticky cookie isAdded to the buffered map.httptraceWroteHeaders/WroteRequestcallbacks flipready=true.Set-Cookie) onto the real writer (Header()now returns it).WriteHeader→ the merge loop assignsheaders["Set-Cookie"] = ["<sticky>"], replacing the backend's cookies.Reproduction
Traefik (tested v3.6.11) + this plugin as a local plugin, file provider: a router with the sablier middleware (dynamic strategy, Sablier answering
X-Sablier-Session-Status: ready) in front of a service withsticky.cookieenabled, backend answering302+Set-Cookie: TESTSTATE=abc123.curl -sD - http://proxy/(no sticky cookie sent) → response contains only the sticky cookie;TESTSTATEis gone.TESTSTATEpasses through (Traefik doesn't rewrite the sticky cookie).Proposed fix
Set-Cookieis multi-valued — each value is an independent cookie, so merging by assignment is always lossy. Append the buffered values instead of overwriting for this header (other headers keep the current overwrite semantics):PR with the fix + regression tests (unit-level on
responseWriterand end-to-end throughServeHTTPwith anhttptrace-firing next handler): #58