From 18f717d7170de9864d564a5b8c13c33855e575cc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 12 Mar 2026 13:41:06 +0000
Subject: [PATCH 01/17] Bump tornado from 6.5.4 to 6.5.5 (#19551)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.4 to
6.5.5.
Changelog
Sourced from tornado's
changelog.
Release notes
.. toctree::
:maxdepth: 2
releases/v6.5.5
releases/v6.5.4
releases/v6.5.3
releases/v6.5.2
releases/v6.5.1
releases/v6.5.0
releases/v6.4.2
releases/v6.4.1
releases/v6.4.0
releases/v6.3.3
releases/v6.3.2
releases/v6.3.1
releases/v6.3.0
releases/v6.2.0
releases/v6.1.0
releases/v6.0.4
releases/v6.0.3
releases/v6.0.2
releases/v6.0.1
releases/v6.0.0
releases/v5.1.1
releases/v5.1.0
releases/v5.0.2
releases/v5.0.1
releases/v5.0.0
releases/v4.5.3
releases/v4.5.2
releases/v4.5.1
releases/v4.5.0
releases/v4.4.3
releases/v4.4.2
releases/v4.4.1
releases/v4.4.0
releases/v4.3.0
releases/v4.2.1
releases/v4.2.0
releases/v4.1.0
releases/v4.0.2
releases/v4.0.1
releases/v4.0.0
releases/v3.2.2
releases/v3.2.1
releases/v3.2.0
releases/v3.1.1
... (truncated)
Commits
7d64650
Merge pull request #3586
from bdarnell/update-cibw
d05d59b
build: Bump cibuildwheel to 3.4.0
c2f4673
Merge pull request #3585
from bdarnell/release-655
e5f1aa4
Release notes and version bump for v6.5.5
78a046f
httputil: Add CRLF to _FORBIDDEN_HEADER_CHARS_RE
24a2d96
web: Validate characters in all cookie attributes.
119a195
httputil: Add limits on multipart form data parsing
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
poetry.lock | 68 ++++++++++++++++++++++++++---------------------------
1 file changed, 33 insertions(+), 35 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 0d03c860f5..f5088ea9b9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -31,7 +31,7 @@ description = "The ultimate Python library in building OAuth and OpenID Connect
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"oidc\" or extra == \"jwt\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"jwt\" or extra == \"oidc\""
files = [
{file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"},
{file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"},
@@ -531,7 +531,7 @@ description = "XML bomb protection for Python stdlib modules"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -556,7 +556,7 @@ description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and l
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "elementpath-4.8.0-py3-none-any.whl", hash = "sha256:5393191f84969bcf8033b05ec4593ef940e58622ea13cefe60ecefbbf09d58d9"},
{file = "elementpath-4.8.0.tar.gz", hash = "sha256:5822a2560d99e2633d95f78694c7ff9646adaa187db520da200a8e9479dc46ae"},
@@ -606,7 +606,7 @@ description = "Python wrapper for hiredis"
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"redis\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"redis\""
files = [
{file = "hiredis-3.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9937d9b69321b393fbace69f55423480f098120bc55a3316e1ca3508c4dbbd6f"},
{file = "hiredis-3.3.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:50351b77f89ba6a22aff430b993653847f36b71d444509036baa0f2d79d1ebf4"},
@@ -930,7 +930,7 @@ description = "Jaeger Python OpenTracing Tracer implementation"
optional = true
python-versions = ">=3.7"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "jaeger-client-4.8.0.tar.gz", hash = "sha256:3157836edab8e2c209bd2d6ae61113db36f7ee399e66b1dcbb715d87ab49bfe0"},
]
@@ -1122,7 +1122,7 @@ description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\""
files = [
{file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"},
{file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"},
@@ -1239,7 +1239,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li
optional = true
python-versions = ">=3.8"
groups = ["main"]
-markers = "extra == \"url-preview\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"url-preview\""
files = [
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"},
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"},
@@ -1553,7 +1553,7 @@ description = "An LDAP3 auth provider for Synapse"
optional = true
python-versions = ">=3.10"
groups = ["main"]
-markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\""
files = [
{file = "matrix_synapse_ldap3-0.4.0-py3-none-any.whl", hash = "sha256:bf080037230d2af5fd3639cb87266de65c1cad7a68ea206278c5b4bf9c1a17f3"},
{file = "matrix_synapse_ldap3-0.4.0.tar.gz", hash = "sha256:cff52ba780170de5e6e8af42863d2648ee23f3bf0a9fea6db52372f9fc00be2b"},
@@ -1834,7 +1834,7 @@ description = "OpenTracing API for Python. See documentation at http://opentraci
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "opentracing-2.4.0.tar.gz", hash = "sha256:a173117e6ef580d55874734d1fa7ecb6f3655160b8b8974a2a1e98e5ec9c840d"},
]
@@ -2032,7 +2032,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"postgres\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"postgres\""
files = [
{file = "psycopg2-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:103e857f46bb76908768ead4e2d0ba1d1a130e7b8ed77d3ae91e8b33481813e8"},
{file = "psycopg2-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:210daed32e18f35e3140a1ebe059ac29209dd96468f2f7559aa59f75ee82a5cb"},
@@ -2050,7 +2050,7 @@ description = ".. image:: https://travis-ci.org/chtd/psycopg2cffi.svg?branch=mas
optional = true
python-versions = "*"
groups = ["main"]
-markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")"
+markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")"
files = [
{file = "psycopg2cffi-2.9.0.tar.gz", hash = "sha256:7e272edcd837de3a1d12b62185eb85c45a19feda9e62fa1b120c54f9e8d35c52"},
]
@@ -2066,7 +2066,7 @@ description = "A Simple library to enable psycopg2 compatability"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")"
+markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")"
files = [
{file = "psycopg2cffi-compat-1.1.tar.gz", hash = "sha256:d25e921748475522b33d13420aad5c2831c743227dc1f1f2585e0fdb5c914e05"},
]
@@ -2348,7 +2348,7 @@ description = "A development tool to measure, monitor and analyze the memory beh
optional = true
python-versions = ">=3.6"
groups = ["main"]
-markers = "extra == \"cache-memory\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"cache-memory\""
files = [
{file = "Pympler-1.0.1-py3-none-any.whl", hash = "sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d"},
{file = "Pympler-1.0.1.tar.gz", hash = "sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa"},
@@ -2480,7 +2480,7 @@ description = "Python implementation of SAML Version 2 Standard"
optional = true
python-versions = ">=3.9,<4.0"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "pysaml2-7.5.0-py3-none-any.whl", hash = "sha256:bc6627cc344476a83c757f440a73fda1369f13b6fda1b4e16bca63ffbabb5318"},
{file = "pysaml2-7.5.0.tar.gz", hash = "sha256:f36871d4e5ee857c6b85532e942550d2cf90ea4ee943d75eb681044bbc4f54f7"},
@@ -2505,7 +2505,7 @@ description = "Extensions to the standard Python datetime module"
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -2533,7 +2533,7 @@ description = "World timezone definitions, modern and historical"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"},
{file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"},
@@ -2937,7 +2937,7 @@ description = "Python client for Sentry (https://sentry.io)"
optional = true
python-versions = ">=3.6"
groups = ["main"]
-markers = "extra == \"sentry\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"sentry\""
files = [
{file = "sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de"},
{file = "sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b"},
@@ -3136,7 +3136,7 @@ description = "Tornado IOLoop Backed Concurrent Futures"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "threadloop-1.0.2-py2-none-any.whl", hash = "sha256:5c90dbefab6ffbdba26afb4829d2a9df8275d13ac7dc58dccb0e279992679599"},
{file = "threadloop-1.0.2.tar.gz", hash = "sha256:8b180aac31013de13c2ad5c834819771992d350267bddb854613ae77ef571944"},
@@ -3152,7 +3152,7 @@ description = "Python bindings for the Apache Thrift RPC system"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
{file = "thrift-0.22.0.tar.gz", hash = "sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466"},
]
@@ -3222,25 +3222,23 @@ markers = {main = "python_version < \"3.14\""}
[[package]]
name = "tornado"
-version = "6.5.4"
+version = "6.5.5"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = true
python-versions = ">=3.9"
groups = ["main"]
-markers = "extra == \"opentracing\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"opentracing\""
files = [
- {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9"},
- {file = "tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335"},
- {file = "tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f"},
- {file = "tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8"},
- {file = "tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1"},
- {file = "tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc"},
- {file = "tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1"},
- {file = "tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7"},
+ {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa"},
+ {file = "tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521"},
+ {file = "tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5"},
+ {file = "tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07"},
+ {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e"},
+ {file = "tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca"},
+ {file = "tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7"},
+ {file = "tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b"},
+ {file = "tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6"},
+ {file = "tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9"},
]
[[package]]
@@ -3361,7 +3359,7 @@ description = "non-blocking redis client for python"
optional = true
python-versions = "*"
groups = ["main"]
-markers = "extra == \"redis\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"redis\""
files = [
{file = "txredisapi-1.4.11-py3-none-any.whl", hash = "sha256:ac64d7a9342b58edca13ef267d4fa7637c1aa63f8595e066801c1e8b56b22d0b"},
{file = "txredisapi-1.4.11.tar.gz", hash = "sha256:3eb1af99aefdefb59eb877b1dd08861efad60915e30ad5bf3d5bf6c5cedcdbc6"},
@@ -3622,7 +3620,7 @@ description = "An XML Schema validator and decoder"
optional = true
python-versions = ">=3.7"
groups = ["main"]
-markers = "extra == \"saml2\" or extra == \"all\""
+markers = "extra == \"all\" or extra == \"saml2\""
files = [
{file = "xmlschema-2.5.1-py3-none-any.whl", hash = "sha256:ec2b2a15c8896c1fcd14dcee34ca30032b99456c3c43ce793fdb9dca2fb4b869"},
{file = "xmlschema-2.5.1.tar.gz", hash = "sha256:4f7497de6c8b6dc2c28ad7b9ed6e21d186f4afe248a5bea4f54eedab4da44083"},
From 8d03a4df11aa122e39c0698b6a9b7e0d3b9694d7 Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Thu, 12 Mar 2026 15:17:13 +0100
Subject: [PATCH 02/17] Avoid re-computing the event ID when cloning events.
(#19527)
`event_id` is a lazily-computed property on events, as it's a hash of
the event content on room version 3 and later. The reason we do this is
that it helps finding database inconsistencies by not trusting the event
ID we got from the database.
The thing is, when we clone events (to return them through /sync or
/messages for example) we don't copy the computed hash if we already
computed it, duplicating the work. This copies the internal `_event_id`
property.
---
changelog.d/19527.misc | 1 +
synapse/events/utils.py | 10 +++++++++-
2 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/19527.misc
diff --git a/changelog.d/19527.misc b/changelog.d/19527.misc
new file mode 100644
index 0000000000..c349af286b
--- /dev/null
+++ b/changelog.d/19527.misc
@@ -0,0 +1 @@
+Avoid re-computing the event ID when cloning events.
diff --git a/synapse/events/utils.py b/synapse/events/utils.py
index b79a68f589..1bf4d632c0 100644
--- a/synapse/events/utils.py
+++ b/synapse/events/utils.py
@@ -48,7 +48,7 @@ from synapse.api.room_versions import RoomVersion
from synapse.logging.opentracing import SynapseTags, set_tag, trace
from synapse.types import JsonDict, Requester
-from . import EventBase, StrippedStateEvent, make_event_from_dict
+from . import EventBase, FrozenEventV2, StrippedStateEvent, make_event_from_dict
if TYPE_CHECKING:
from synapse.handlers.relations import BundledAggregations
@@ -109,6 +109,14 @@ def clone_event(event: EventBase) -> EventBase:
event.get_dict(), event.room_version, event.internal_metadata.get_dict()
)
+ # Starting FrozenEventV2, the event ID is an (expensive) hash of the event. This is
+ # lazily computed when we get the FrozenEventV2.event_id property, then cached in
+ # _event_id field. Later FrozenEvent formats all inherit from FrozenEventV2, so we
+ # can use the same logic here.
+ if isinstance(event, FrozenEventV2) and isinstance(new_event, FrozenEventV2):
+ # If we already pre-computed the event ID, use it.
+ new_event._event_id = event._event_id
+
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`.
new_event.internal_metadata.stream_ordering = (
event.internal_metadata.stream_ordering
From 3ce5508c7e1a7277987e368f1fce4804d51a887c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 12 Mar 2026 15:44:21 +0100
Subject: [PATCH 03/17] Bump quinn-proto from 0.11.12 to 0.11.14 (#19544)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.12 to
0.11.14.
Release notes
Sourced from quinn-proto's
releases.
quinn-proto 0.11.14
@jxs reported a
denial of service issue in quinn-proto 5 days ago:
We coordinated with them to release this version to patch the issue.
Unfortunately the maintainers missed these issues during code review and
we did not have enough fuzzing coverage -- we regret the oversight and
have added an additional fuzzing target.
Organizations that want to participate in coordinated disclosure can
contact us privately to discuss terms.
What's Changed
Commits
2c315aa
proto: bump version to 0.11.14
8ad47f4
Use newer rustls-pki-types PEM parser API
c81c028
ci: fix workflow syntax
0050172
ci: pin wasm-bindgen-cli version
8a6f82c
Take semver-compatible dependency updates
e52db4a
Apply suggestions from clippy 1.91
6df7275
chore: Fix unnecessary_unwrap clippy
c8eefa0
proto: avoid unwrapping varint decoding during parameters parsing
9723a97
fuzz: add fuzzing target for parsing transport parameters
eaf0ef3
Fix over-permissive proto dependency edge (#2385)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Cargo.lock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 340114f801..eed90d6471 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -910,9 +910,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.11.12"
+version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.3",
From 4c475dcd7a566367708c59642d17cf7fdd3c0507 Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Thu, 12 Mar 2026 18:11:09 +0100
Subject: [PATCH 04/17] Allow the caching of the /versions and /auth_metadata
endpoints (#19530)
Can be reviewed commit by commit.
This sets caching headers on the /versions and /auth_metadata endpoints
to:
- allow clients to cache the response for up to 10 minutes
(`max-age=600`)
- allow proxies to cache the response for up to an hour
(`s-maxage=3600`)
- make proxies serve stale response for up to an hour (`s-maxage=3600`)
but make them refresh their response after 10 minutes
(`stale-while-revalidate=600`) so that we always have a snappy response
to client, but also have fresh responses most of the time
- only cache the response for unauthenticated requests on /versions
(`Vary: Authorization`)
I'm not too worried about the 1h TTL on the proxy side, as with the
`stale-while-revalidate` directive, one just needs to do two requests
after 10 minutes to get a fresh response from the cache.
The reason we want this, is that clients usually load this right away,
leading to a lot of traffic from people just loading the Element Web
login screen with the default config. This is currently routed to
`client_readers` on matrix.org (and ESS) which can be overwhelmed for
other reasons, leading to slow response times on those endpoints (3s+).
Overwhelmed workers shouldn't prevent people from logging in, and
shouldn't result in a long loading spinner in clients. This PR allows
caching proxies (like Cloudflare) to publicly cache the unauthenticated
response of those two endpoints and make it load quicker, reducing
server load as well.
---
changelog.d/19530.misc | 1 +
synapse/http/server.py | 26 +++++++++++++++++--
synapse/rest/client/auth_metadata.py | 38 ++++++++++++++++++++++++++++
synapse/rest/client/versions.py | 20 +++++++++++++++
4 files changed, 83 insertions(+), 2 deletions(-)
create mode 100644 changelog.d/19530.misc
diff --git a/changelog.d/19530.misc b/changelog.d/19530.misc
new file mode 100644
index 0000000000..9e5bc0fe04
--- /dev/null
+++ b/changelog.d/19530.misc
@@ -0,0 +1 @@
+Allow caching of the `/versions` and `/auth_metadata` public endpoints.
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 226cb00831..2c235e04f4 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -861,7 +861,18 @@ def respond_with_json(
encoder = _encode_json_bytes
request.setHeader(b"Content-Type", b"application/json")
- request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ # Insert a default Cache-Control header if the servlet hasn't already set one. The
+ # default directive tells both the client and any intermediary cache to not cache
+ # the response, which is a sensible default to have on most API endpoints.
+ # The absence `Cache-Control` header would mean that it's up to the clients and
+ # caching proxies mood to cache things if they want. This can be dangerous, which is
+ # why we explicitly set a "don't cache by default" policy.
+ # In practice, `no-store` should be enough, but having all three directives is more
+ # conservative in case we encounter weird, non-spec compliant caches.
+ # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
+ # for more details.
+ if not request.responseHeaders.hasHeader(b"Cache-Control"):
+ request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
if send_cors:
set_cors_headers(request)
@@ -901,7 +912,18 @@ def respond_with_json_bytes(
request.setHeader(b"Content-Type", b"application/json")
request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
- request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ # Insert a default Cache-Control header if the servlet hasn't already set one. The
+ # default directive tells both the client and any intermediary cache to not cache
+ # the response, which is a sensible default to have on most API endpoints.
+ # The absence `Cache-Control` header would mean that it's up to the clients and
+ # caching proxies mood to cache things if they want. This can be dangerous, which is
+ # why we explicitly set a "don't cache by default" policy.
+ # In practice, `no-store` should be enough, but having all three directives is more
+ # conservative in case we encounter weird, non-spec compliant caches.
+ # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
+ # for more details.
+ if not request.responseHeaders.hasHeader(b"Cache-Control"):
+ request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
if send_cors:
set_cors_headers(request)
diff --git a/synapse/rest/client/auth_metadata.py b/synapse/rest/client/auth_metadata.py
index 702f550906..062b8ed13e 100644
--- a/synapse/rest/client/auth_metadata.py
+++ b/synapse/rest/client/auth_metadata.py
@@ -49,6 +49,25 @@ class AuthIssuerServlet(RestServlet):
self._auth = hs.get_auth()
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
+ # This endpoint is unauthenticated and the response only depends on
+ # the metadata we get from Matrix Authentication Service. Internally,
+ # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
+ # response in memory anyway. Ideally we would follow any Cache-Control directive
+ # given by MAS, but this is fine for now.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
if self._config.mas.enabled:
assert isinstance(self._auth, MasDelegatedAuth)
return 200, {"issuer": await self._auth.issuer()}
@@ -94,6 +113,25 @@ class AuthMetadataServlet(RestServlet):
self._auth = hs.get_auth()
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
+ # This endpoint is unauthenticated and the response only depends on
+ # the metadata we get from Matrix Authentication Service. Internally,
+ # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the
+ # response in memory anyway. Ideally we would follow any Cache-Control directive
+ # given by MAS, but this is fine for now.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
if self._config.mas.enabled:
assert isinstance(self._auth, MasDelegatedAuth)
return 200, await self._auth.auth_metadata()
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index 23f5ffeedb..f8d7a1a4d9 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -81,6 +81,26 @@ class VersionsRestServlet(RestServlet):
msc3575_enabled = await self.store.is_feature_enabled(
user_id, ExperimentalFeature.MSC3575
)
+ else:
+ # Allow caching of unauthenticated responses, as they only depend
+ # on server configuration which rarely changes.
+ #
+ # - `public` means it can be cached both in the browser and in caching proxies
+ # - `max-age` controls how long we cache on the browser side. 10m is sane enough
+ # - `s-maxage` controls how long we cache on the proxy side. Since caching
+ # proxies usually have a way to purge caches, it is fine to cache there for
+ # longer (1h), and issue cache invalidations in case we need it
+ # - `stale-while-revalidate` allows caching proxies to serve stale content while
+ # revalidating in the background. This is useful for making this request always
+ # 'snappy' to end users whilst still keeping it fresh
+ request.setHeader(
+ b"Cache-Control",
+ b"public, max-age=600, s-maxage=3600, stale-while-revalidate=600",
+ )
+
+ # Tell caches to vary on the Authorization header, so that
+ # authenticated responses are not served from cache.
+ request.setHeader(b"Vary", b"Authorization")
return (
200,
From c0924fbbd8ba6c3d9c0984a05a3160494abc7fbd Mon Sep 17 00:00:00 2001
From: Andrew Ferrazzutti
Date: Mon, 16 Mar 2026 12:29:42 -0400
Subject: [PATCH 05/17] MSC4140: put delay_id in unsigned data for sender
(#19479)
Implements
https://github.com/matrix-org/matrix-spec-proposals/pull/4140/changes/49b200dcc11de286974925177b1e184cd905e6fa
---
changelog.d/19479.feature | 1 +
rust/src/events/internal_metadata.rs | 22 +++++
synapse/events/utils.py | 79 +++++++++--------
synapse/handlers/delayed_events.py | 2 +
synapse/handlers/message.py | 13 ++-
synapse/handlers/room_member.py | 10 +++
synapse/synapse_rust/events.pyi | 2 +
tests/rest/client/test_delayed_events.py | 108 ++++++++++++++++++++++-
8 files changed, 198 insertions(+), 39 deletions(-)
create mode 100644 changelog.d/19479.feature
diff --git a/changelog.d/19479.feature b/changelog.d/19479.feature
new file mode 100644
index 0000000000..3e7e8bd6ff
--- /dev/null
+++ b/changelog.d/19479.feature
@@ -0,0 +1 @@
+[MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender.
diff --git a/rust/src/events/internal_metadata.rs b/rust/src/events/internal_metadata.rs
index fa40fdcfad..595f9cf7eb 100644
--- a/rust/src/events/internal_metadata.rs
+++ b/rust/src/events/internal_metadata.rs
@@ -57,6 +57,7 @@ enum EventInternalMetadataData {
PolicyServerSpammy(bool),
Redacted(bool),
TxnId(Box),
+ DelayId(Box),
TokenId(i64),
DeviceId(Box),
}
@@ -115,6 +116,10 @@ impl EventInternalMetadataData {
pyo3::intern!(py, "txn_id"),
o.into_pyobject(py).unwrap_infallible().into_any(),
),
+ EventInternalMetadataData::DelayId(o) => (
+ pyo3::intern!(py, "delay_id"),
+ o.into_pyobject(py).unwrap_infallible().into_any(),
+ ),
EventInternalMetadataData::TokenId(o) => (
pyo3::intern!(py, "token_id"),
o.into_pyobject(py).unwrap_infallible().into_any(),
@@ -179,6 +184,12 @@ impl EventInternalMetadataData {
.map(String::into_boxed_str)
.with_context(|| format!("'{key_str}' has invalid type"))?,
),
+ "delay_id" => EventInternalMetadataData::DelayId(
+ value
+ .extract()
+ .map(String::into_boxed_str)
+ .with_context(|| format!("'{key_str}' has invalid type"))?,
+ ),
"token_id" => EventInternalMetadataData::TokenId(
value
.extract()
@@ -472,6 +483,17 @@ impl EventInternalMetadata {
set_property!(self, TxnId, obj.into_boxed_str());
}
+ /// The delay ID, set only if the event was a delayed event.
+ #[getter]
+ fn get_delay_id(&self) -> PyResult<&str> {
+ let s = get_property!(self, DelayId)?;
+ Ok(s)
+ }
+ #[setter]
+ fn set_delay_id(&mut self, obj: String) {
+ set_property!(self, DelayId, obj.into_boxed_str());
+ }
+
/// The access token ID of the user who sent this event, if any.
#[getter]
fn get_token_id(&self) -> PyResult {
diff --git a/synapse/events/utils.py b/synapse/events/utils.py
index 1bf4d632c0..89eb2182af 100644
--- a/synapse/events/utils.py
+++ b/synapse/events/utils.py
@@ -420,7 +420,7 @@ class SerializeEventConfig:
# Function to convert from federation format to client format
event_format: Callable[[JsonDict], JsonDict] = format_event_for_client_v1
# The entity that requested the event. This is used to determine whether to include
- # the transaction_id in the unsigned section of the event.
+ # the transaction_id and delay_id in the unsigned section of the event.
requester: Requester | None = None
# List of event fields to include. If empty, all fields will be returned.
only_event_fields: list[str] | None = None
@@ -483,44 +483,49 @@ def serialize_event(
config=config,
)
- # If we have a txn_id saved in the internal_metadata, we should include it in the
- # unsigned section of the event if it was sent by the same session as the one
- # requesting the event.
- txn_id: str | None = getattr(e.internal_metadata, "txn_id", None)
- if (
- txn_id is not None
- and config.requester is not None
- and config.requester.user.to_string() == e.sender
- ):
- # Some events do not have the device ID stored in the internal metadata,
- # this includes old events as well as those created by appservice, guests,
- # or with tokens minted with the admin API. For those events, fallback
- # to using the access token instead.
- event_device_id: str | None = getattr(e.internal_metadata, "device_id", None)
- if event_device_id is not None:
- if event_device_id == config.requester.device_id:
- d["unsigned"]["transaction_id"] = txn_id
+ # If we have applicable fields saved in the internal_metadata, include them in the
+ # unsigned section of the event if the event was sent by the same session (or when
+ # appropriate, just the same sender) as the one requesting the event.
+ if config.requester is not None and config.requester.user.to_string() == e.sender:
+ txn_id: str | None = getattr(e.internal_metadata, "txn_id", None)
+ if txn_id is not None:
+ # Some events do not have the device ID stored in the internal metadata,
+ # this includes old events as well as those created by appservice, guests,
+ # or with tokens minted with the admin API. For those events, fallback
+ # to using the access token instead.
+ event_device_id: str | None = getattr(
+ e.internal_metadata, "device_id", None
+ )
+ if event_device_id is not None:
+ if event_device_id == config.requester.device_id:
+ d["unsigned"]["transaction_id"] = txn_id
- else:
- # Fallback behaviour: only include the transaction ID if the event
- # was sent from the same access token.
- #
- # For regular users, the access token ID can be used to determine this.
- # This includes access tokens minted with the admin API.
- #
- # For guests and appservice users, we can't check the access token ID
- # so assume it is the same session.
- event_token_id: int | None = getattr(e.internal_metadata, "token_id", None)
- if (
- (
- event_token_id is not None
- and config.requester.access_token_id is not None
- and event_token_id == config.requester.access_token_id
+ else:
+ # Fallback behaviour: only include the transaction ID if the event
+ # was sent from the same access token.
+ #
+ # For regular users, the access token ID can be used to determine this.
+ # This includes access tokens minted with the admin API.
+ #
+ # For guests and appservice users, we can't check the access token ID
+ # so assume it is the same session.
+ event_token_id: int | None = getattr(
+ e.internal_metadata, "token_id", None
)
- or config.requester.is_guest
- or config.requester.app_service
- ):
- d["unsigned"]["transaction_id"] = txn_id
+ if (
+ (
+ event_token_id is not None
+ and config.requester.access_token_id is not None
+ and event_token_id == config.requester.access_token_id
+ )
+ or config.requester.is_guest
+ or config.requester.app_service
+ ):
+ d["unsigned"]["transaction_id"] = txn_id
+
+ delay_id: str | None = getattr(e.internal_metadata, "delay_id", None)
+ if delay_id is not None:
+ d["unsigned"]["org.matrix.msc4140.delay_id"] = delay_id
# invite_room_state and knock_room_state are a list of stripped room state events
# that are meant to provide metadata about a room to an invitee/knocker. They are
diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py
index 7e41716f1e..4a9f646d4d 100644
--- a/synapse/handlers/delayed_events.py
+++ b/synapse/handlers/delayed_events.py
@@ -560,6 +560,7 @@ class DelayedEventsHandler:
action=membership,
content=event.content,
origin_server_ts=event.origin_server_ts,
+ delay_id=event.delay_id,
)
else:
event_dict: JsonDict = {
@@ -585,6 +586,7 @@ class DelayedEventsHandler:
requester,
event_dict,
txn_id=txn_id,
+ delay_id=event.delay_id,
)
event_id = sent_event.event_id
except ShadowBanError:
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 99ce120736..eb01622515 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -585,6 +585,7 @@ class EventCreationHandler:
state_map: StateMap[str] | None = None,
for_batch: bool = False,
current_state_group: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, UnpersistedEventContextBase]:
"""
Given a dict from a client, create a new event. If bool for_batch is true, will
@@ -600,7 +601,7 @@ class EventCreationHandler:
Args:
requester
event_dict: An entire event
- txn_id
+ txn_id: The transaction ID.
prev_event_ids:
the forward extremities to use as the prev_events for the
new event.
@@ -639,6 +640,8 @@ class EventCreationHandler:
current_state_group: the current state group, used only for creating events for
batch persisting
+ delay_id: The delay ID of this event, if it was a delayed event.
+
Raises:
ResourceLimitError if server is blocked to some resource being
exceeded
@@ -726,6 +729,9 @@ class EventCreationHandler:
if txn_id is not None:
builder.internal_metadata.txn_id = txn_id
+ if delay_id is not None:
+ builder.internal_metadata.delay_id = delay_id
+
builder.internal_metadata.outlier = outlier
event, unpersisted_context = await self.create_new_client_event(
@@ -966,6 +972,7 @@ class EventCreationHandler:
ignore_shadow_ban: bool = False,
outlier: bool = False,
depth: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, int]:
"""
Creates an event, then sends it.
@@ -994,6 +1001,7 @@ class EventCreationHandler:
depth: Override the depth used to order the event in the DAG.
Should normally be set to None, which will cause the depth to be calculated
based on the prev_events.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
The event, and its stream ordering (if deduplication happened,
@@ -1090,6 +1098,7 @@ class EventCreationHandler:
ignore_shadow_ban=ignore_shadow_ban,
outlier=outlier,
depth=depth,
+ delay_id=delay_id,
)
async def _create_and_send_nonmember_event_locked(
@@ -1103,6 +1112,7 @@ class EventCreationHandler:
ignore_shadow_ban: bool = False,
outlier: bool = False,
depth: int | None = None,
+ delay_id: str | None = None,
) -> tuple[EventBase, int]:
room_id = event_dict["room_id"]
@@ -1131,6 +1141,7 @@ class EventCreationHandler:
state_event_ids=state_event_ids,
outlier=outlier,
depth=depth,
+ delay_id=delay_id,
)
context = await unpersisted_context.persist(event)
diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 0c6be72716..b2e678e90e 100644
--- a/synapse/handlers/room_member.py
+++ b/synapse/handlers/room_member.py
@@ -408,6 +408,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent: bool = True,
outlier: bool = False,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""
Internal membership update function to get an existing event or create
@@ -440,6 +441,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
opposed to being inline with the current DAG.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
Tuple of event ID and stream ordering position
@@ -492,6 +494,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
depth=depth,
require_consent=require_consent,
outlier=outlier,
+ delay_id=delay_id,
)
context = await unpersisted_context.persist(event)
prev_state_ids = await context.get_prev_state_ids(
@@ -587,6 +590,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids: list[str] | None = None,
depth: int | None = None,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""Update a user's membership in a room.
@@ -617,6 +621,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
A tuple of the new event ID and stream ID.
@@ -679,6 +684,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids=state_event_ids,
depth=depth,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
return result
@@ -701,6 +707,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
state_event_ids: list[str] | None = None,
depth: int | None = None,
origin_server_ts: int | None = None,
+ delay_id: str | None = None,
) -> tuple[str, int]:
"""Helper for update_membership.
@@ -733,6 +740,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
+ delay_id: The delay ID of this event, if it was a delayed event.
Returns:
A tuple of the new event ID and stream ID.
@@ -943,6 +951,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent=require_consent,
outlier=outlier,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
latest_event_ids = await self.store.get_prev_events_for_room(room_id)
@@ -1201,6 +1210,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
require_consent=require_consent,
outlier=outlier,
origin_server_ts=origin_server_ts,
+ delay_id=delay_id,
)
async def check_for_any_membership_in_room(
diff --git a/synapse/synapse_rust/events.pyi b/synapse/synapse_rust/events.pyi
index 0add391c65..185f29694b 100644
--- a/synapse/synapse_rust/events.pyi
+++ b/synapse/synapse_rust/events.pyi
@@ -38,6 +38,8 @@ class EventInternalMetadata:
txn_id: str
"""The transaction ID, if it was set when the event was created."""
+ delay_id: str
+ """The delay ID, set only if the event was a delayed event."""
token_id: int
"""The access token ID of the user who sent this event, if any."""
device_id: str
diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py
index efa69a393a..da904ce1f5 100644
--- a/tests/rest/client/test_delayed_events.py
+++ b/tests/rest/client/test_delayed_events.py
@@ -22,7 +22,7 @@ from twisted.internet.testing import MemoryReactor
from synapse.api.errors import Codes
from synapse.rest import admin
-from synapse.rest.client import delayed_events, login, room, versions
+from synapse.rest.client import delayed_events, login, room, sync, versions
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util.clock import Clock
@@ -59,6 +59,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
delayed_events.register_servlets,
login.register_servlets,
room.register_servlets,
+ sync.register_servlets,
]
def default_config(self) -> JsonDict:
@@ -106,6 +107,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
+
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
content = self._get_delayed_event_content(events[0])
@@ -128,6 +132,56 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
+ def test_delayed_member_events_are_sent_on_timeout(self) -> None:
+ channel = self.make_request(
+ "PUT",
+ _get_path_for_delayed_state(
+ self.room_id,
+ "m.room.member",
+ self.user2_user_id,
+ 900,
+ ),
+ {
+ "membership": "leave",
+ "reason": "Delayed kick",
+ },
+ self.user1_access_token,
+ )
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
+
+ events = self._get_delayed_events()
+ self.assertEqual(1, len(events), events)
+ content = self._get_delayed_event_content(events[0])
+ self.assertEqual("leave", content.get("membership"), content)
+ self.assertEqual("Delayed kick", content.get("reason"), content)
+
+ content = self.helper.get_state(
+ self.room_id,
+ "m.room.member",
+ self.user1_access_token,
+ state_key=self.user2_user_id,
+ )
+ self.assertEqual("join", content.get("membership"), content)
+
+ self.reactor.advance(1)
+ self.assertListEqual([], self._get_delayed_events())
+ content = self.helper.get_state(
+ self.room_id,
+ "m.room.member",
+ self.user1_access_token,
+ state_key=self.user2_user_id,
+ )
+ self.assertEqual("leave", content.get("membership"), content)
+ self.assertEqual("Delayed kick", content.get("reason"), content)
+
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def test_get_delayed_events_auth(self) -> None:
channel = self.make_request("GET", PATH_PREFIX)
self.assertEqual(HTTPStatus.UNAUTHORIZED, channel.code, channel.result)
@@ -254,6 +308,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
expect_code=HTTPStatus.NOT_FOUND,
)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, False)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config(
{"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}}
@@ -327,6 +384,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(content_value, content.get(content_property_name), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config({"rc_message": {"per_second": 2.5, "burst_count": 3}})
def test_send_delayed_event_ratelimit(self, action_in_path: bool) -> None:
@@ -406,6 +466,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
@parameterized.expand((True, False))
@unittest.override_config(
{"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}}
@@ -450,6 +513,8 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
@@ -474,6 +539,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, True)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def test_delayed_state_is_cancelled_by_new_state_from_other_user(
self,
) -> None:
@@ -489,6 +557,8 @@ class DelayedEventsTestCase(HomeserverTestCase):
self.user1_access_token,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ delay_id = channel.json_body.get("delay_id")
+ assert delay_id is not None
events = self._get_delayed_events()
self.assertEqual(1, len(events), events)
@@ -513,6 +583,9 @@ class DelayedEventsTestCase(HomeserverTestCase):
)
self.assertEqual(setter_expected, content.get(setter_key), content)
+ self._find_sent_delayed_event(self.user1_access_token, delay_id, False)
+ self._find_sent_delayed_event(self.user2_access_token, delay_id, False)
+
def _get_delayed_events(self) -> list[JsonDict]:
channel = self.make_request(
"GET",
@@ -549,6 +622,39 @@ class DelayedEventsTestCase(HomeserverTestCase):
body["action"] = action
return self.make_request("POST", path, body)
+ def _find_sent_delayed_event(
+ self, access_token: str, delay_id: str, should_find: bool
+ ) -> None:
+ """Call /sync and look for a synced event with a specified delay_id.
+ At most one event will ever have a matching delay_id.
+
+ Args:
+ access_token: The access token of the user to call /sync for.
+ delay_id: The delay_id to search for in synced events.
+ should_find: Whether /sync should include an event with a matching delay_id.
+ """
+ channel = self.make_request("GET", "/sync", access_token=access_token)
+ self.assertEqual(HTTPStatus.OK, channel.code)
+
+ rooms = channel.json_body["rooms"]
+ events = []
+ for membership in "join", "leave":
+ if membership in rooms:
+ events += rooms[membership][self.room_id]["timeline"]["events"]
+
+ found = False
+ for event in events:
+ if event["unsigned"].get("org.matrix.msc4140.delay_id") == delay_id:
+ if not should_find:
+ self.fail(
+ "Found event with matching delay_id, but expected to not find one"
+ )
+ if found:
+ self.fail("Found multiple events with matching delay_id")
+ found = True
+ if should_find and not found:
+ self.fail("Did not find event with matching delay_id")
+
def _get_path_for_delayed_state(
room_id: str, event_type: str, state_key: str, delay_ms: int
From eedd4c8796b7abdbeea5d6622e0c45c3056f5fe9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 16 Mar 2026 18:30:42 +0100
Subject: [PATCH 06/17] Bump pyjwt from 2.11.0 to 2.12.0 (#19560)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.11.0 to 2.12.0.
Release notes
Sourced from pyjwt's
releases.
2.12.0
Security
What's Changed
New Contributors
Full Changelog: https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0
Changelog
Sourced from pyjwt's
changelog.
v2.12.0
<https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0>__
Fixed
- Annotate PyJWKSet.keys for pyright by @tamird in
`[#1134](https://github.com/jpadilla/pyjwt/issues/1134)
<https://github.com/jpadilla/pyjwt/pull/1134>`__
- Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python
3.14 by @veeceey in
`[#1133](https://github.com/jpadilla/pyjwt/issues/1133)
<https://github.com/jpadilla/pyjwt/pull/1133>`__
- Do not keep ``algorithms`` dict in PyJWK instances by @akx in
`[#1143](https://github.com/jpadilla/pyjwt/issues/1143)
<https://github.com/jpadilla/pyjwt/pull/1143>`__
- Validate the crit (Critical) Header Parameter defined in RFC 7515
§4.1.11. by @dmbs335 in `GHSA-752w-5fwx-jx9f
<https://github.com/jpadilla/pyjwt/security/advisories/GHSA-752w-5fwx-jx9f>`__
- Use PyJWK algorithm when encoding without explicit algorithm in
`[#1148](https://github.com/jpadilla/pyjwt/issues/1148)
<https://github.com/jpadilla/pyjwt/pull/1148>`__
Added
- Docs: Add
PyJWKClient API reference and document the
two-tier caching system (JWK Set cache and signing key LRU cache).
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
poetry.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index f5088ea9b9..0bcb88a046 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2306,14 +2306,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyjwt"
-version = "2.11.0"
+version = "2.12.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"},
- {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"},
+ {file = "pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e"},
+ {file = "pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02"},
]
[package.dependencies]
From cdd261b1c60b96fbe690e8bc0aa9dd6c76cb5530 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 16 Mar 2026 17:35:35 +0000
Subject: [PATCH 07/17] Bump pyopenssl from 25.3.0 to 26.0.0 (#19574)
Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 25.3.0 to
26.0.0.
Changelog
Sourced from pyopenssl's
changelog.
26.0.0 (2026-03-15)
Backward-incompatible changes:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Dropped support for Python 3.7.
- The minimum
cryptography version is now 46.0.0.
Deprecations:
^^^^^^^^^^^^^
Changes:
^^^^^^^^
- Added support for using aws-lc instead of OpenSSL.
- Properly raise an error if a DTLS cookie callback returned a cookie
longer than
DTLS1_COOKIE_LENGTH bytes. Previously this
would result in a buffer-overflow. Credit to dark_haxor
for reporting the issue. CVE-2026-27459
- Added
OpenSSL.SSL.Connection.get_group_name to
determine which group name was negotiated.
Context.set_tlsext_servername_callback now handles
exceptions raised in the callback by calling sys.excepthook
and returning a fatal TLS alert. Previously, exceptions were silently
swallowed and the handshake would proceed as if the callback had
succeeded. Credit to Leury Castillo for reporting this
issue. CVE-2026-27448
Commits
358cbf2
Prepare for 26.0.0 release (#1487)
a8d28e7
Bump actions/cache from 4 to 5 (#1486)
6fefff0
Add aws-lc compatibility to tests and CI (#1476)
a739f96
Bump actions/download-artifact from 8.0.0 to 8.0.1 (#1485)
8b4c66b
Bump actions/upload-artifact in /.github/actions/upload-coverage (#1484)
02a5c78
Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#1483)
d973387
Bump actions/download-artifact from 7.0.0 to 8.0.0 (#1482)
57f09bb
Fix buffer overflow in DTLS cookie generation callback (#1479)
d41a814
Handle exceptions in set_tlsext_servername_callback callbacks (#1478)
7b29beb
Fix not using a cryptography wheel on uv (#1475)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/element-hq/synapse/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
poetry.lock | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 0bcb88a046..15cc023f22 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2398,18 +2398,18 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
[[package]]
name = "pyopenssl"
-version = "25.3.0"
+version = "26.0.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6"},
- {file = "pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329"},
+ {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
+ {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
]
[package.dependencies]
-cryptography = ">=45.0.7,<47"
+cryptography = ">=46.0.0,<47"
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
[package.extras]
From a71c468b04f308809b489e230edf3a1e91f23d0c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 16 Mar 2026 17:52:13 +0000
Subject: [PATCH 08/17] Bump the patches group with 2 updates (#19536)
Bumps the patches group with 2 updates:
[anyhow](https://github.com/dtolnay/anyhow) and
[pyo3-log](https://github.com/vorner/pyo3-log).
Updates `anyhow` from 1.0.101 to 1.0.102
Release notes
Sourced from anyhow's
releases.
1.0.102
Commits
5c657b3
Release 1.0.102
e737fb6
Merge pull request #442
from dtolnay/backtrace
7fe62b5
Further simply backtrace conditional compilation
c8cb5ca
Merge pull request #441
from dtolnay/backtrace
de27df7
Delete CI use of --features=backtrace
9b67e5d
Merge pull request #440
from dtolnay/backtrace
efdb11a
Simplify std_backtrace conditional code
b8a9a70
Merge pull request #439
from dtolnay/backtrace
a42fc2c
Remove feature = "backtrace" conditional
code
2a2a3ce
Re-word backtrace feature comment
- Additional commits viewable in compare
view
Updates `pyo3-log` from 0.13.2 to 0.13.3
Changelog
Sourced from pyo3-log's
changelog.
0.13.3
- Support for pyo3 0.28 (#75).
Commits
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Cargo.lock | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index eed90d6471..d6945bfbb7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.101"
+version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
@@ -844,9 +844,9 @@ dependencies = [
[[package]]
name = "pyo3-log"
-version = "0.13.2"
+version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f8bae9ad5ba08b0b0ed2bb9c2bdbaeccc69cafca96d78cf0fbcea0d45d122bb"
+checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e"
dependencies = [
"arc-swap",
"log",
From 3aa948c50c1f0b1d6e87ea7f222cc155dea77e3b Mon Sep 17 00:00:00 2001
From: Olivier 'reivilibre
Date: Mon, 16 Mar 2026 18:27:54 +0000
Subject: [PATCH 09/17] When Matrix Authentication Service (MAS) integration is
enabled, allow MAS to set the user locked status in Synapse. (#19554)
Companion PR:
https://github.com/element-hq/matrix-authentication-service/pull/5550
to 1) send this flag
and 2) provision users proactively when their lock status changes.
---
Currently Synapse and MAS have two independent user lock
implementations. This PR makes it so that MAS can push its lock status
to Synapse when 'provisioning' the user.
Having the lock status in Synapse is useful for removing users from the
user directory
when they are locked.
There is otherwise no authentication requirement to have it in Synapse;
the enforcement is done
by MAS at token introspection time.
---------
Signed-off-by: Olivier 'reivilibre
---
changelog.d/19554.feature | 1 +
synapse/rest/synapse/mas/users.py | 15 +++++++
tests/rest/synapse/mas/test_users.py | 58 ++++++++++++++++++++++++++++
3 files changed, 74 insertions(+)
create mode 100644 changelog.d/19554.feature
diff --git a/changelog.d/19554.feature b/changelog.d/19554.feature
new file mode 100644
index 0000000000..d30d3da1f6
--- /dev/null
+++ b/changelog.d/19554.feature
@@ -0,0 +1 @@
+When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse.
\ No newline at end of file
diff --git a/synapse/rest/synapse/mas/users.py b/synapse/rest/synapse/mas/users.py
index 55c7337555..01db41bcfa 100644
--- a/synapse/rest/synapse/mas/users.py
+++ b/synapse/rest/synapse/mas/users.py
@@ -112,6 +112,18 @@ class MasProvisionUserResource(MasBaseResource):
unset_emails: StrictBool = False
set_emails: list[StrictStr] | None = None
+ locked: StrictBool | None = None
+ """
+ True to lock user. False to unlock. None to leave the same.
+
+ This is mostly for informational purposes; if the user's account is locked in MAS
+ but not in Synapse, the token introspection response will prevent them from using
+ their account.
+
+ However, having a local copy of the locked state in Synapse is useful for excluding
+ the user from the user directory.
+ """
+
@model_validator(mode="before")
@classmethod
def validate_exclusive(cls, values: Any) -> Any:
@@ -206,6 +218,9 @@ class MasProvisionUserResource(MasBaseResource):
validated_at=current_time,
)
+ if body.locked is not None:
+ await self.store.set_user_locked_status(user_id.to_string(), body.locked)
+
if body.unset_avatar_url:
await self.profile_handler.set_avatar_url(
target_user=user_id,
diff --git a/tests/rest/synapse/mas/test_users.py b/tests/rest/synapse/mas/test_users.py
index f0f26a939c..6f44761bb8 100644
--- a/tests/rest/synapse/mas/test_users.py
+++ b/tests/rest/synapse/mas/test_users.py
@@ -362,6 +362,64 @@ class MasProvisionUserResource(BaseTestCase):
)
self.assertEqual(channel.code, 400, f"Should fail for content: {content}")
+ def test_lock_and_unlock(self) -> None:
+ store = self.hs.get_datastores().main
+
+ # Create a user in the locked state
+ alice = UserID("alice", "test")
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": True,
+ },
+ )
+ # This created the user, hence the 201 status code
+ self.assertEqual(channel.code, 201, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertTrue(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
+ # Then transition from locked to unlocked
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": False,
+ },
+ )
+ # This updated the user, hence the 200 status code
+ self.assertEqual(channel.code, 200, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertFalse(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
+ # And back from unlocked to locked
+ channel = self.make_request(
+ "POST",
+ "/_synapse/mas/provision_user",
+ shorthand=False,
+ access_token=self.SHARED_SECRET,
+ content={
+ "localpart": alice.localpart,
+ "locked": True,
+ },
+ )
+ # This updated the user, hence the 200 status code
+ self.assertEqual(channel.code, 200, channel.json_body)
+ self.assertEqual(channel.json_body, {})
+ self.assertTrue(
+ self.get_success(store.get_user_locked_status(alice.to_string()))
+ )
+
@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasIsLocalpartAvailableResource(BaseTestCase):
From 6254e009bbcf5991a1490645a77a5bd4addbbb80 Mon Sep 17 00:00:00 2001
From: Eric Eastwood
Date: Mon, 16 Mar 2026 21:56:16 -0500
Subject: [PATCH 10/17] Fix `Build and push complement image` CI job pointing
to non-existent image (#19523)
:x:
https://github.com/element-hq/synapse/actions/runs/22609655282/job/65509315002#step:8:39
```
Error response from daemon: No such image: complement-synapse:latest
```
Regressed in
https://github.com/element-hq/synapse/pull/19475#discussion_r2823157623
where we updated `complement.sh` to build `localhost/complement-synapse`
instead of `complement-synapse`.
---
.github/workflows/push_complement_image.yml | 1 +
changelog.d/19523.bugfix | 1 +
scripts-dev/complement.sh | 13 +++++++++----
3 files changed, 11 insertions(+), 4 deletions(-)
create mode 100644 changelog.d/19523.bugfix
diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml
index 12b4720ca5..40ff3ba0e0 100644
--- a/.github/workflows/push_complement_image.yml
+++ b/.github/workflows/push_complement_image.yml
@@ -69,6 +69,7 @@ jobs:
run: |
for TAG in ${{ join(fromJson(steps.meta.outputs.json).tags, ' ') }}; do
echo "tag and push $TAG"
+ # `localhost/complement-synapse` should match the image created by `scripts-dev/complement.sh`
docker tag complement-synapse $TAG
docker push $TAG
done
diff --git a/changelog.d/19523.bugfix b/changelog.d/19523.bugfix
new file mode 100644
index 0000000000..e9f53c61ba
--- /dev/null
+++ b/changelog.d/19523.bugfix
@@ -0,0 +1 @@
+Fix `Build and push complement image` CI job pointing to non-existent image.
diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh
index c65ae53df0..a8a361fd4a 100755
--- a/scripts-dev/complement.sh
+++ b/scripts-dev/complement.sh
@@ -38,17 +38,22 @@ set -e
# Tag local builds with a dummy registry namespace so that later builds may reference
# them exactly instead of accidentally pulling from a remote registry.
#
-# This is important as some storage drivers/types prefer remote images over local
-# (`containerd`) which causes problems as we're testing against some remote image that
-# doesn't include all of the changes that we're trying to test (be it locally or in a PR
-# in CI). This is spawning from a real-world problem where the GitHub runners were
+# This is important as some Docker storage drivers/types prefer remote images over local
+# (like `containerd`) which causes problems as we're testing against some remote image
+# that doesn't include all of the changes that we're trying to test (be it locally or in
+# a PR in CI). This is spawning from a real-world problem where the GitHub runners were
# updated to use Docker Engine 29.0.0+ which uses `containerd` by default for new
# installations.
+#
+# XXX: If the Docker image name changes, don't forget to update
+# `.github/workflows/push_complement_image.yml` as well
LOCAL_IMAGE_NAMESPACE=localhost
# The image tags for how these images will be stored in the registry
SYNAPSE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse"
SYNAPSE_WORKERS_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-workers"
+# XXX: If the Docker image name changes, don't forget to update
+# `.github/workflows/push_complement_image.yml` as well
COMPLEMENT_SYNAPSE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/complement-synapse"
SYNAPSE_EDITABLE_IMAGE_PATH="$LOCAL_IMAGE_NAMESPACE/synapse-editable"
From c37a5bb4cdb8894bb87b2229992faa4daa29ee33 Mon Sep 17 00:00:00 2001
From: Eric Eastwood
Date: Mon, 16 Mar 2026 22:20:56 -0500
Subject: [PATCH 11/17] Restore `localhost/complement-synapse` change from
#19523
See https://github.com/element-hq/synapse/pull/19523#discussion_r2944133700
---
.github/workflows/push_complement_image.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml
index 40ff3ba0e0..7793172e55 100644
--- a/.github/workflows/push_complement_image.yml
+++ b/.github/workflows/push_complement_image.yml
@@ -70,6 +70,6 @@ jobs:
for TAG in ${{ join(fromJson(steps.meta.outputs.json).tags, ' ') }}; do
echo "tag and push $TAG"
# `localhost/complement-synapse` should match the image created by `scripts-dev/complement.sh`
- docker tag complement-synapse $TAG
+ docker tag localhost/complement-synapse $TAG
docker push $TAG
done
From 8a6d9a8d4550c3b0f6232ef5a2a5bfa51fbc5534 Mon Sep 17 00:00:00 2001
From: Andrew Ferrazzutti
Date: Tue, 17 Mar 2026 10:36:07 -0400
Subject: [PATCH 12/17] Admin API docs: use consistent path param syntax
(#19307)
Always use `/` instead of sometimes using `/$param`
### Pull Request Checklist
* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
- Use markdown where necessary, mostly for `code blocks`.
- End with either a period (.) or an exclamation mark (!).
- Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
---
changelog.d/19307.doc | 1 +
docs/admin_api/user_admin_api.md | 12 ++++++------
2 files changed, 7 insertions(+), 6 deletions(-)
create mode 100644 changelog.d/19307.doc
diff --git a/changelog.d/19307.doc b/changelog.d/19307.doc
new file mode 100644
index 0000000000..a27e97f3d0
--- /dev/null
+++ b/changelog.d/19307.doc
@@ -0,0 +1 @@
+In the Admin API documentation, always express path parameters as `/` instead of as `/$param`.
diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md
index 72e0e8d91a..14f86fa976 100644
--- a/docs/admin_api/user_admin_api.md
+++ b/docs/admin_api/user_admin_api.md
@@ -600,7 +600,7 @@ Fetches the number of invites sent by the provided user ID across all rooms
after the given timestamp.
```
-GET /_synapse/admin/v1/users/$user_id/sent_invite_count
+GET /_synapse/admin/v1/users//sent_invite_count
```
**Parameters**
@@ -634,7 +634,7 @@ Fetches the number of rooms that the user joined after the given timestamp, even
if they have subsequently left/been banned from those rooms.
```
-GET /_synapse/admin/v1/users/$/cumulative_joined_room_count
```
**Parameters**
@@ -1439,7 +1439,7 @@ The request and response format is the same as the
The API is:
```
-GET /_synapse/admin/v1/auth_providers/$provider/users/$external_id
+GET /_synapse/admin/v1/auth_providers//users/
```
When a user matched the given ID for the given provider, an HTTP code `200` with a response body like the following is returned:
@@ -1478,7 +1478,7 @@ _Added in Synapse 1.68.0._
The API is:
```
-GET /_synapse/admin/v1/threepid/$medium/users/$address
+GET /_synapse/admin/v1/threepid//users/
```
When a user matched the given address for the given medium, an HTTP code `200` with a response body like the following is returned:
@@ -1522,7 +1522,7 @@ is provided to override the default and allow the admin to issue the redactions
The API is
```
-POST /_synapse/admin/v1/user/$user_id/redact
+POST /_synapse/admin/v1/user//redact
{
"rooms": ["!roomid1", "!roomid2"]
@@ -1571,7 +1571,7 @@ or until Synapse is restarted (whichever happens first).
The API is:
```
-GET /_synapse/admin/v1/user/redact_status/$redact_id
+GET /_synapse/admin/v1/user/redact_status/
```
A response body like the following is returned:
From 8ad7e8af81bd655deb2014a7df04cf218c8a829d Mon Sep 17 00:00:00 2001
From: Eric Eastwood
Date: Tue, 17 Mar 2026 09:43:05 -0500
Subject: [PATCH 13/17] Add some light labels to the `Processed request` logs
(#19548)
It's pretty hard to remember the order of all of these ambiguous
numbers. I assume they're not totally labeled already to cut down on the
length when scanning with your eyes. This just adds a few hints of what
each grouping is.
Spawning from [staring at some Synapse
logs](https://github.com/element-hq/matrix-hosted/issues/10631) and
cross-referencing the Synapse source code over and over.
---
changelog.d/19548.misc | 1 +
docs/upgrade.md | 8 ++++++++
docs/usage/administration/request_log.md | 4 ++--
synapse/http/site.py | 4 +++-
4 files changed, 14 insertions(+), 3 deletions(-)
create mode 100644 changelog.d/19548.misc
diff --git a/changelog.d/19548.misc b/changelog.d/19548.misc
new file mode 100644
index 0000000000..eed4d5d8e2
--- /dev/null
+++ b/changelog.d/19548.misc
@@ -0,0 +1 @@
+Add a few labels to the number groupings in the `Processed request` logs.
diff --git a/docs/upgrade.md b/docs/upgrade.md
index 777e57c492..aeae82c114 100644
--- a/docs/upgrade.md
+++ b/docs/upgrade.md
@@ -146,6 +146,14 @@ No immediate change is necessary, however once the parameter is removed, modules
From this version, when the parameter is passed, an error such as
``Deprecated `deactivation` parameter passed to `set_displayname` Module API (value: False). This will break in 2027.`` will be logged. The method will otherwise continue to work.
+## Updated request log format (`Processed request: ...`)
+
+The [request log format](usage/administration/request_log.md) has slightly changed to
+include `ru=(...)` and `db=(...)` labels to better disambiguate the number groupings.
+Previously, these values appeared without labels.
+
+This only matters if you have third-party tooling that parses the Synapse logs.
+
# Upgrading to v1.146.0
## Drop support for Ubuntu 25.04 Plucky Puffin, and add support for 25.10 Questing Quokka
diff --git a/docs/usage/administration/request_log.md b/docs/usage/administration/request_log.md
index 6154108934..7e3047eb62 100644
--- a/docs/usage/administration/request_log.md
+++ b/docs/usage/administration/request_log.md
@@ -5,8 +5,8 @@ HTTP request logs are written by synapse (see [`synapse/http/site.py`](https://g
See the following for how to decode the dense data available from the default logging configuration.
```
-2020-10-01 12:00:00,000 - synapse.access.http.8008 - 311 - INFO - PUT-1000- 192.168.0.1 - 8008 - {another-matrix-server.com} Processed request: 0.100sec/-0.000sec (0.000sec, 0.000sec) (0.001sec/0.090sec/3) 11B !200 "PUT /_matrix/federation/v1/send/1600000000000 HTTP/1.1" "Synapse/1.20.1" [0 dbevts]
--AAAAAAAAAAAAAAAAAAAAA- -BBBBBBBBBBBBBBBBBBBBBB- -C- -DD- -EEEEEE- -FFFFFFFFF- -GG- -HHHHHHHHHHHHHHHHHHHHHHH- -IIIIII- -JJJJJJJ- -KKKKKK-, -LLLLLL- -MMMMMMM- -NNNNNN- O -P- -QQ- -RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR- -SSSSSSSSSSSS- -TTTTTT-
+2020-10-01 12:00:00,000 - synapse.access.http.8008 - 311 - INFO - PUT-1000- 192.168.0.1 - 8008 - {another-matrix-server.com} Processed request: 0.100sec/-0.000sec ru=(0.000sec, 0.000sec) db=(0.001sec/0.090sec/3) 11B !200 "PUT /_matrix/federation/v1/send/1600000000000 HTTP/1.1" "Synapse/1.20.1" [0 dbevts]
+-AAAAAAAAAAAAAAAAAAAAA- -BBBBBBBBBBBBBBBBBBBBBB- -C- -DD- -EEEEEE- -FFFFFFFFF- -GG- -HHHHHHHHHHHHHHHHHHHHHHH- -IIIIII- -JJJJJJJ- -KKKKKK-, -LLLLLL- -MMMMMM- -NNNNNN- O -P- -QQ- -RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR- -SSSSSSSSSSSS- -TTTTTT-
```
diff --git a/synapse/http/site.py b/synapse/http/site.py
index 6ced5b98b3..9b7fd5c936 100644
--- a/synapse/http/site.py
+++ b/synapse/http/site.py
@@ -638,10 +638,12 @@ class SynapseRequest(Request):
if authenticated_entity:
requester = f"{authenticated_entity}|{requester}"
+ # Updates to this log line should also be reflected in our docs,
+ # `docs/usage/administration/request_log.md`
self.synapse_site.access_logger.log(
log_level,
"%s - %s - {%s}"
- " Processed request: %.3fsec/%.3fsec (%.3fsec, %.3fsec) (%.3fsec/%.3fsec/%d)"
+ " Processed request: %.3fsec/%.3fsec ru=(%.3fsec, %.3fsec) db=(%.3fsec/%.3fsec/%d)"
' %sB %s "%s %s %s" "%s" [%d dbevts]',
self.get_client_ip_if_available(),
self.synapse_site.site_tag,
From 6a63f0dcd7e59df6acceb9ee8049325a99481c5b Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Tue, 17 Mar 2026 15:45:28 +0100
Subject: [PATCH 14/17] Migrate dev dependencies to PEP 735 dependency groups
(#19490)
This moves the dev dependencies to PEP 735 dependency groups, to help us
move to standard project metadata, which will help us moving to `uv`
(#19566)
This requires poetry 2.2.0
---
.github/workflows/fix_lint.yaml | 2 +-
.github/workflows/latest_deps.yml | 2 +-
.github/workflows/tests.yml | 18 +++----
.github/workflows/twisted_trunk.yml | 4 +-
changelog.d/19490.misc | 1 +
debian/build_virtualenv | 2 +-
debian/changelog | 1 +
docker/Dockerfile | 2 +-
docs/development/dependencies.md | 2 +-
mypy.ini | 2 +-
poetry.lock | 2 +-
pyproject.toml | 83 +++++++++++++++--------------
12 files changed, 63 insertions(+), 58 deletions(-)
create mode 100644 changelog.d/19490.misc
diff --git a/.github/workflows/fix_lint.yaml b/.github/workflows/fix_lint.yaml
index babc3bc5de..4752b6afeb 100644
--- a/.github/workflows/fix_lint.yaml
+++ b/.github/workflows/fix_lint.yaml
@@ -31,7 +31,7 @@ jobs:
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
install-project: "false"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- name: Run ruff check
continue-on-error: true
diff --git a/.github/workflows/latest_deps.yml b/.github/workflows/latest_deps.yml
index 5bc78062cd..2d945c2096 100644
--- a/.github/workflows/latest_deps.yml
+++ b/.github/workflows/latest_deps.yml
@@ -54,7 +54,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: "3.x"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "all"
# Dump installed versions for debugging.
- run: poetry run pip list > before.txt
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b39c453959..a03d27472d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -95,7 +95,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: "3.x"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "all"
- run: poetry run scripts-dev/generate_sample_config.sh --check
- run: poetry run scripts-dev/config-lint.sh
@@ -134,7 +134,7 @@ jobs:
- name: Setup Poetry
uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
install-project: "false"
- name: Run ruff check
@@ -169,7 +169,7 @@ jobs:
# https://github.com/matrix-org/synapse/pull/15376#issuecomment-1498983775
# To make CI green, err towards caution and install the project.
install-project: "true"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
# Cribbed from
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
@@ -265,7 +265,7 @@ jobs:
# Install like a normal project from source with all optional dependencies
extras: all
install-project: "true"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- name: Ensure `Cargo.lock` is up to date (no stray changes after install)
# The `::error::` syntax is using GitHub Actions' error annotations, see
@@ -398,7 +398,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.job.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: ${{ matrix.job.extras }}
- name: Await PostgreSQL
if: ${{ matrix.job.postgres-version }}
@@ -500,7 +500,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: ${{ matrix.extras }}
- run: poetry run trial --jobs=2 tests
- name: Dump logs
@@ -595,7 +595,7 @@ jobs:
- run: sudo apt-get -qq install xmlsec1 postgresql-client
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "postgres"
- run: .ci/scripts/test_export_data_command.sh
env:
@@ -648,7 +648,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
python-version: ${{ matrix.python-version }}
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
extras: "postgres"
- run: .ci/scripts/test_synapse_port_db.sh
id: run_tester_script
@@ -708,7 +708,7 @@ jobs:
# We use `poetry` in `complement.sh`
- uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
with:
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
# Matches the `path` where we checkout Synapse above
working-directory: "synapse"
diff --git a/.github/workflows/twisted_trunk.yml b/.github/workflows/twisted_trunk.yml
index 12fdbbe7c4..d38e38ebcb 100644
--- a/.github/workflows/twisted_trunk.yml
+++ b/.github/workflows/twisted_trunk.yml
@@ -54,7 +54,7 @@ jobs:
with:
python-version: "3.x"
extras: "all"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- run: |
poetry remove twisted
poetry add --extras tls git+https://github.com/twisted/twisted.git#${{ inputs.twisted_ref || 'trunk' }}
@@ -82,7 +82,7 @@ jobs:
with:
python-version: "3.x"
extras: "all test"
- poetry-version: "2.1.1"
+ poetry-version: "2.2.1"
- run: |
poetry remove twisted
poetry add --extras tls git+https://github.com/twisted/twisted.git#trunk
diff --git a/changelog.d/19490.misc b/changelog.d/19490.misc
new file mode 100644
index 0000000000..924197a1cb
--- /dev/null
+++ b/changelog.d/19490.misc
@@ -0,0 +1 @@
+Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups.
diff --git a/debian/build_virtualenv b/debian/build_virtualenv
index 70d4efcbd0..7bbf52ddd9 100755
--- a/debian/build_virtualenv
+++ b/debian/build_virtualenv
@@ -35,7 +35,7 @@ TEMP_VENV="$(mktemp -d)"
python3 -m venv "$TEMP_VENV"
source "$TEMP_VENV/bin/activate"
pip install -U pip
-pip install poetry==2.1.1 poetry-plugin-export==1.9.0
+pip install poetry==2.2.1 poetry-plugin-export==1.9.0
poetry export \
--extras all \
--extras test \
diff --git a/debian/changelog b/debian/changelog
index 9cd39ee76c..1a21a105b8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,6 +1,7 @@
matrix-synapse-py3 (1.149.1+nmu1) UNRELEASED; urgency=medium
* Change how the systemd journald integration is installed.
+ * Update Poetry used at build time to 2.2.1.
-- Quentin Gliech Fri, 20 Feb 2026 19:19:51 +0100
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 59771ae88f..6070d5c355 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -22,7 +22,7 @@
ARG DEBIAN_VERSION=trixie
ARG PYTHON_VERSION=3.13
-ARG POETRY_VERSION=2.1.1
+ARG POETRY_VERSION=2.2.1
###
### Stage 0: generate requirements.txt
diff --git a/docs/development/dependencies.md b/docs/development/dependencies.md
index 1b3348703f..fe0667194a 100644
--- a/docs/development/dependencies.md
+++ b/docs/development/dependencies.md
@@ -6,7 +6,7 @@ This is a quick cheat sheet for developers on how to use [`poetry`](https://pyth
See the [contributing guide](contributing_guide.md#4-install-the-dependencies).
-Developers should use Poetry 1.3.2 or higher. If you encounter problems related
+Developers should use Poetry 2.2.0 or higher. If you encounter problems related
to poetry, please [double-check your poetry version](#check-the-version-of-poetry-with-poetry---version).
# Background
diff --git a/mypy.ini b/mypy.ini
index d6a3434293..ec73ce9f6e 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -69,7 +69,7 @@ warn_unused_ignores = False
;; https://github.com/python/typeshed/tree/master/stubs
;; and for each package `foo` there's a corresponding `types-foo` package on PyPI,
;; which we can pull in as a dev dependency by adding to `pyproject.toml`'s
-;; `[tool.poetry.group.dev.dependencies]` list.
+;; `[dependency-groups]` `dev` list.
# https://github.com/lepture/authlib/issues/460
[mypy-authlib.*]
diff --git a/poetry.lock b/poetry.lock
index 15cc023f22..681a183b42 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -3754,4 +3754,4 @@ url-preview = ["lxml"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10.0,<4.0.0"
-content-hash = "dd63614889e7e181fca33760741a490e65fe4ef4f42756cafd0f804ae7324916"
+content-hash = "ce9ac9da9e7ffaf24b3e1e7892342ba486e7af4ea25385f875d0f3a2d5c5d133"
diff --git a/pyproject.toml b/pyproject.toml
index 07ab1ee5db..f655a1ef8b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -224,6 +224,9 @@ update_synapse_database = "synapse._scripts.update_synapse_database:main"
[tool.poetry]
packages = [{ include = "synapse" }]
+# We're using PEP 735 dependency groups, which requires Poetry 2.2.0+
+requires-poetry = ">=2.2.0"
+
[tool.poetry.build]
# Compile our rust module when using `poetry install`. This is still required
# while using `poetry` as the build frontend. Saves the developer from needing
@@ -251,55 +254,55 @@ generate-setup-file = true
# Dependencies used for developing Synapse itself.
#
-# Hold off on migrating these to `dev-dependencies` (PEP 735) for now until
-# Poetry 2.2.0+, pip 25.1+ are more widely available.
-[tool.poetry.group.dev.dependencies]
# We pin development dependencies in poetry.lock so that our tests don't start
# failing on new releases. Keeping lower bounds loose here means that dependabot
# can bump versions without having to update the content-hash in the lockfile.
# This helps prevents merge conflicts when running a batch of dependabot updates.
-ruff = "0.14.6"
+[dependency-groups]
+dev = [
+ "ruff==0.14.6",
-# Typechecking
-lxml-stubs = ">=0.4.0"
-mypy = "*"
-mypy-zope = "*"
-types-bleach = ">=4.1.0"
-types-jsonschema = ">=3.2.0"
-types-netaddr = ">=0.8.0.6"
-types-opentracing = ">=2.4.2"
-types-Pillow = ">=8.3.4"
-types-psycopg2 = ">=2.9.9"
-types-pyOpenSSL = ">=20.0.7"
-types-PyYAML = ">=5.4.10"
-types-requests = ">=2.26.0"
-types-setuptools = ">=57.4.0"
+ # Typechecking
+ "lxml-stubs>=0.4.0",
+ "mypy",
+ "mypy-zope",
+ "types-bleach>=4.1.0",
+ "types-jsonschema>=3.2.0",
+ "types-netaddr>=0.8.0.6",
+ "types-opentracing>=2.4.2",
+ "types-Pillow>=8.3.4",
+ "types-psycopg2>=2.9.9",
+ "types-pyOpenSSL>=20.0.7",
+ "types-PyYAML>=5.4.10",
+ "types-requests>=2.26.0",
+ "types-setuptools>=57.4.0",
-# Dependencies which are exclusively required by unit test code. This is
-# NOT a list of all modules that are necessary to run the unit tests.
-# Tests assume that all optional dependencies are installed.
-#
-# If this is updated, don't forget to update the equivalent lines in
-# project.optional-dependencies.test.
-parameterized = ">=0.9.0"
-idna = ">=3.3"
+ # Dependencies which are exclusively required by unit test code. This is
+ # NOT a list of all modules that are necessary to run the unit tests.
+ # Tests assume that all optional dependencies are installed.
+ #
+ # If this is updated, don't forget to update the equivalent lines in
+ # project.optional-dependencies.test.
+ "parameterized>=0.9.0",
+ "idna>=3.3",
-# The following are used by the release script
-click = ">=8.1.3"
-# GitPython was == 3.1.14; bumped to 3.1.20, the first release with type hints.
-GitPython = ">=3.1.20"
-markdown-it-py = ">=3.0.0"
-pygithub = ">=1.59"
-# The following are executed as commands by the release script.
-twine = "*"
-# Towncrier min version comes from https://github.com/matrix-org/synapse/pull/3425. Rationale unclear.
-towncrier = ">=18.6.0rc1"
+ # The following are used by the release script
+ "click>=8.1.3",
+ # GitPython was == 3.1.14; bumped to 3.1.20, the first release with type hints.
+ "GitPython>=3.1.20",
+ "markdown-it-py>=3.0.0",
+ "pygithub>=1.59",
+ # The following are executed as commands by the release script.
+ "twine",
+ # Towncrier min version comes from https://github.com/matrix-org/synapse/pull/3425. Rationale unclear.
+ "towncrier>=18.6.0rc1",
-# Used for checking the Poetry lockfile
-tomli = ">=1.2.3"
+ # Used for checking the Poetry lockfile
+ "tomli>=1.2.3",
-# Used for checking the schema delta files
-sqlglot = ">=28.0.0"
+ # Used for checking the schema delta files
+ "sqlglot>=28.0.0",
+]
[tool.towncrier]
package = "synapse"
From 7d8e8747eade7314f083ff4c9d9ef2d0becb5124 Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Tue, 17 Mar 2026 15:56:55 +0100
Subject: [PATCH 15/17] 1.150.0rc1
---
CHANGES.md | 33 +++++++++++++++++++++++++++++++
changelog.d/19307.doc | 1 -
changelog.d/19311.misc | 1 -
changelog.d/19314.feature | 1 -
changelog.d/19476.misc | 1 -
changelog.d/19479.feature | 1 -
changelog.d/19487.feature | 1 -
changelog.d/19490.misc | 1 -
changelog.d/19491.misc | 1 -
changelog.d/19517.doc | 1 -
changelog.d/19518.doc | 1 -
changelog.d/19523.bugfix | 1 -
changelog.d/19527.misc | 1 -
changelog.d/19530.misc | 1 -
changelog.d/19542.bugfix | 1 -
changelog.d/19548.misc | 1 -
changelog.d/19554.feature | 1 -
debian/changelog | 8 ++++++--
pyproject.toml | 2 +-
schema/synapse-config.schema.yaml | 2 +-
20 files changed, 41 insertions(+), 20 deletions(-)
delete mode 100644 changelog.d/19307.doc
delete mode 100644 changelog.d/19311.misc
delete mode 100644 changelog.d/19314.feature
delete mode 100644 changelog.d/19476.misc
delete mode 100644 changelog.d/19479.feature
delete mode 100644 changelog.d/19487.feature
delete mode 100644 changelog.d/19490.misc
delete mode 100644 changelog.d/19491.misc
delete mode 100644 changelog.d/19517.doc
delete mode 100644 changelog.d/19518.doc
delete mode 100644 changelog.d/19523.bugfix
delete mode 100644 changelog.d/19527.misc
delete mode 100644 changelog.d/19530.misc
delete mode 100644 changelog.d/19542.bugfix
delete mode 100644 changelog.d/19548.misc
delete mode 100644 changelog.d/19554.feature
diff --git a/CHANGES.md b/CHANGES.md
index 1a5ff136c8..e7c2416238 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,36 @@
+# Synapse 1.150.0rc1 (2026-03-17)
+
+## Features
+
+- Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint. ([\#19314](https://github.com/element-hq/synapse/issues/19314))
+- [MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender. ([\#19479](https://github.com/element-hq/synapse/issues/19479))
+- Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over the legacy (v3) /sync API. ([\#19487](https://github.com/element-hq/synapse/issues/19487))
+- When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. ([\#19554](https://github.com/element-hq/synapse/issues/19554))
+
+## Bugfixes
+
+- Fix `Build and push complement image` CI job pointing to non-existent image. ([\#19523](https://github.com/element-hq/synapse/issues/19523))
+- Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory. ([\#19542](https://github.com/element-hq/synapse/issues/19542))
+
+## Improved Documentation
+
+- In the Admin API documentation, always express path parameters as `/` instead of as `/$param`. ([\#19307](https://github.com/element-hq/synapse/issues/19307))
+- Update docs to clarify `outbound_federation_restricted_to` can also be used with the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways). ([\#19517](https://github.com/element-hq/synapse/issues/19517))
+- Unify Complement developer docs. ([\#19518](https://github.com/element-hq/synapse/issues/19518))
+
+## Internal Changes
+
+- Put membership updates in a background resumable task when changing the avatar or the display name. ([\#19311](https://github.com/element-hq/synapse/issues/19311))
+- Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are). ([\#19476](https://github.com/element-hq/synapse/issues/19476))
+- Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. ([\#19490](https://github.com/element-hq/synapse/issues/19490))
+- Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package. ([\#19491](https://github.com/element-hq/synapse/issues/19491))
+- Avoid re-computing the event ID when cloning events. ([\#19527](https://github.com/element-hq/synapse/issues/19527))
+- Allow caching of the `/versions` and `/auth_metadata` public endpoints. ([\#19530](https://github.com/element-hq/synapse/issues/19530))
+- Add a few labels to the number groupings in the `Processed request` logs. ([\#19548](https://github.com/element-hq/synapse/issues/19548))
+
+
+
+
# Synapse 1.149.1 (2026-03-11)
## Internal Changes
diff --git a/changelog.d/19307.doc b/changelog.d/19307.doc
deleted file mode 100644
index a27e97f3d0..0000000000
--- a/changelog.d/19307.doc
+++ /dev/null
@@ -1 +0,0 @@
-In the Admin API documentation, always express path parameters as `/` instead of as `/$param`.
diff --git a/changelog.d/19311.misc b/changelog.d/19311.misc
deleted file mode 100644
index 66ec86c02d..0000000000
--- a/changelog.d/19311.misc
+++ /dev/null
@@ -1 +0,0 @@
-Put membership updates in a background resumable task when changing the avatar or the display name.
diff --git a/changelog.d/19314.feature b/changelog.d/19314.feature
deleted file mode 100644
index fd2893c577..0000000000
--- a/changelog.d/19314.feature
+++ /dev/null
@@ -1 +0,0 @@
-Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint.
\ No newline at end of file
diff --git a/changelog.d/19476.misc b/changelog.d/19476.misc
deleted file mode 100644
index c1869911a4..0000000000
--- a/changelog.d/19476.misc
+++ /dev/null
@@ -1 +0,0 @@
-Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are).
diff --git a/changelog.d/19479.feature b/changelog.d/19479.feature
deleted file mode 100644
index 3e7e8bd6ff..0000000000
--- a/changelog.d/19479.feature
+++ /dev/null
@@ -1 +0,0 @@
-[MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender.
diff --git a/changelog.d/19487.feature b/changelog.d/19487.feature
deleted file mode 100644
index 4eb1d8f261..0000000000
--- a/changelog.d/19487.feature
+++ /dev/null
@@ -1 +0,0 @@
-Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over the legacy (v3) /sync API.
\ No newline at end of file
diff --git a/changelog.d/19490.misc b/changelog.d/19490.misc
deleted file mode 100644
index 924197a1cb..0000000000
--- a/changelog.d/19490.misc
+++ /dev/null
@@ -1 +0,0 @@
-Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups.
diff --git a/changelog.d/19491.misc b/changelog.d/19491.misc
deleted file mode 100644
index 62b0ddddc2..0000000000
--- a/changelog.d/19491.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package.
diff --git a/changelog.d/19517.doc b/changelog.d/19517.doc
deleted file mode 100644
index 778c14c6aa..0000000000
--- a/changelog.d/19517.doc
+++ /dev/null
@@ -1 +0,0 @@
-Update docs to clarify `outbound_federation_restricted_to` can also be used with the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways).
diff --git a/changelog.d/19518.doc b/changelog.d/19518.doc
deleted file mode 100644
index 5de867c8d7..0000000000
--- a/changelog.d/19518.doc
+++ /dev/null
@@ -1 +0,0 @@
-Unify Complement developer docs.
diff --git a/changelog.d/19523.bugfix b/changelog.d/19523.bugfix
deleted file mode 100644
index e9f53c61ba..0000000000
--- a/changelog.d/19523.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix `Build and push complement image` CI job pointing to non-existent image.
diff --git a/changelog.d/19527.misc b/changelog.d/19527.misc
deleted file mode 100644
index c349af286b..0000000000
--- a/changelog.d/19527.misc
+++ /dev/null
@@ -1 +0,0 @@
-Avoid re-computing the event ID when cloning events.
diff --git a/changelog.d/19530.misc b/changelog.d/19530.misc
deleted file mode 100644
index 9e5bc0fe04..0000000000
--- a/changelog.d/19530.misc
+++ /dev/null
@@ -1 +0,0 @@
-Allow caching of the `/versions` and `/auth_metadata` public endpoints.
diff --git a/changelog.d/19542.bugfix b/changelog.d/19542.bugfix
deleted file mode 100644
index ab72504335..0000000000
--- a/changelog.d/19542.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory.
\ No newline at end of file
diff --git a/changelog.d/19548.misc b/changelog.d/19548.misc
deleted file mode 100644
index eed4d5d8e2..0000000000
--- a/changelog.d/19548.misc
+++ /dev/null
@@ -1 +0,0 @@
-Add a few labels to the number groupings in the `Processed request` logs.
diff --git a/changelog.d/19554.feature b/changelog.d/19554.feature
deleted file mode 100644
index d30d3da1f6..0000000000
--- a/changelog.d/19554.feature
+++ /dev/null
@@ -1 +0,0 @@
-When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse.
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index 1a21a105b8..55af4c22b5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,13 @@
-matrix-synapse-py3 (1.149.1+nmu1) UNRELEASED; urgency=medium
+matrix-synapse-py3 (1.150.0~rc1) stable; urgency=medium
+ [ Quentin Gliech ]
* Change how the systemd journald integration is installed.
* Update Poetry used at build time to 2.2.1.
- -- Quentin Gliech Fri, 20 Feb 2026 19:19:51 +0100
+ [ Synapse Packaging team ]
+ * New synapse release 1.150.0rc1.
+
+ -- Synapse Packaging team Tue, 17 Mar 2026 14:56:35 +0000
matrix-synapse-py3 (1.149.1) stable; urgency=medium
diff --git a/pyproject.toml b/pyproject.toml
index f655a1ef8b..adb9993aae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "matrix-synapse"
-version = "1.149.1"
+version = "1.150.0rc1"
description = "Homeserver for the Matrix decentralised comms protocol"
readme = "README.rst"
authors = [
diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml
index 3a61a4c6fc..dbf7d7acb7 100644
--- a/schema/synapse-config.schema.yaml
+++ b/schema/synapse-config.schema.yaml
@@ -1,5 +1,5 @@
$schema: https://element-hq.github.io/synapse/latest/schema/v1/meta.schema.json
-$id: https://element-hq.github.io/synapse/schema/synapse/v1.149/synapse-config.schema.json
+$id: https://element-hq.github.io/synapse/schema/synapse/v1.150/synapse-config.schema.json
type: object
properties:
modules:
From d65ef848eb14f6e0c69e2a9614776a814d330244 Mon Sep 17 00:00:00 2001
From: Eric Eastwood
Date: Tue, 17 Mar 2026 10:51:53 -0500
Subject: [PATCH 16/17] Fix `Build and push complement image` CI job not having
Poetry for `complement.sh` (#19578)
:x: `Build and push complement image`,
https://github.com/element-hq/synapse/actions/runs/23176317296/job/67339146082
```
scripts-dev/complement.sh: line 227: poetry: command not found
```
Follow-up to https://github.com/element-hq/synapse/pull/19523
This regressed in https://github.com/element-hq/synapse/pull/19476
### Testing strategy
1. Visit
https://github.com/element-hq/synapse/actions/workflows/push_complement_image.yml
1. **Run workflow**:
- **Use workflow from:**
`madlittlemods/fix-complement-push-image-ci-job-poetry`
- **Branch:** `develop`
1. Wait for CI to run and pass :white_check_mark:
---
.github/workflows/push_complement_image.yml | 4 ++++
changelog.d/19578.bugfix | 1 +
2 files changed, 5 insertions(+)
create mode 100644 changelog.d/19578.bugfix
diff --git a/.github/workflows/push_complement_image.yml b/.github/workflows/push_complement_image.yml
index 7793172e55..12599457dc 100644
--- a/.github/workflows/push_complement_image.yml
+++ b/.github/workflows/push_complement_image.yml
@@ -47,6 +47,10 @@ jobs:
if: github.event_name == 'push'
with:
ref: master
+ # We use `poetry` in `complement.sh`
+ - uses: matrix-org/setup-python-poetry@5bbf6603c5c930615ec8a29f1b5d7d258d905aa4 # v2.0.0
+ with:
+ poetry-version: "2.2.1"
- name: Login to registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
diff --git a/changelog.d/19578.bugfix b/changelog.d/19578.bugfix
new file mode 100644
index 0000000000..dbaf4be7e8
--- /dev/null
+++ b/changelog.d/19578.bugfix
@@ -0,0 +1 @@
+Fix `Build and push complement image` CI job not having `poetry` available for the Complement runner script.
From 0d4accb0a6915e97384e0dcebe9c96c975a09f38 Mon Sep 17 00:00:00 2001
From: Olivier 'reivilibre
Date: Tue, 17 Mar 2026 17:08:04 +0000
Subject: [PATCH 17/17] Remove support for MSC3852: Expose user agent
information on Device as the MSC was closed. (#19430)
Fixes: #14836
Discovered whilst looking at the state of MSCs in Synapse.
---------
Signed-off-by: Olivier 'reivilibre
---
changelog.d/19430.removal | 1 +
synapse/config/experimental.py | 3 ---
synapse/handlers/device.py | 1 -
synapse/rest/client/devices.py | 24 ++++--------------------
4 files changed, 5 insertions(+), 24 deletions(-)
create mode 100644 changelog.d/19430.removal
diff --git a/changelog.d/19430.removal b/changelog.d/19430.removal
new file mode 100644
index 0000000000..8920bccc38
--- /dev/null
+++ b/changelog.d/19430.removal
@@ -0,0 +1 @@
+Remove support for [MSC3852: Expose user agent information on Device](https://github.com/matrix-org/matrix-spec-proposals/pull/3852) as the MSC was closed.
\ No newline at end of file
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index a8c9305704..41efff1d8f 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -445,9 +445,6 @@ class ExperimentalConfig(Config):
# MSC3848: Introduce errcodes for specific event sending failures
self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
- # MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
- self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)
-
# MSC3866: M_USER_AWAITING_APPROVAL error code
raw_msc3866_config = experimental.get("msc3866", {})
self.msc3866 = MSC3866Config(**raw_msc3866_config)
diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py
index 29074f5e20..9a371651fb 100644
--- a/synapse/handlers/device.py
+++ b/synapse/handlers/device.py
@@ -129,7 +129,6 @@ class DeviceHandler:
self._auth_handler = hs.get_auth_handler()
self._account_data_handler = hs.get_account_data_handler()
self._event_sources = hs.get_event_sources()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
self._query_appservices_for_keys = (
hs.config.experimental.msc3984_appservice_key_query
)
diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py
index b39ca6a483..4b84131d32 100644
--- a/synapse/rest/client/devices.py
+++ b/synapse/rest/client/devices.py
@@ -55,7 +55,6 @@ class DevicesRestServlet(RestServlet):
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
@@ -63,17 +62,10 @@ class DevicesRestServlet(RestServlet):
requester.user.to_string()
)
- # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
- # removed from each device. If it is enabled, then the field name will
- # be replaced by the unstable identifier.
- #
- # When MSC3852 is accepted, this block of code can just be removed to
- # expose "last_seen_user_agent" to clients.
for device in devices:
- last_seen_user_agent = device["last_seen_user_agent"]
+ # This field is only for admin access and should not be exposed to clients.
+ # (MSC3852, which is closed, did propose to expose it.).
del device["last_seen_user_agent"]
- if self._msc3852_enabled:
- device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
return 200, {"devices": devices}
@@ -144,7 +136,6 @@ class DeviceRestServlet(RestServlet):
handler = hs.get_device_handler()
self.device_handler = handler
self.auth_handler = hs.get_auth_handler()
- self._msc3852_enabled = hs.config.experimental.msc3852_enabled
self._auth_delegation_enabled = (
hs.config.mas.enabled or hs.config.experimental.msc3861.enabled
)
@@ -159,16 +150,9 @@ class DeviceRestServlet(RestServlet):
if device is None:
raise NotFoundError("No device found")
- # If MSC3852 is disabled, then the "last_seen_user_agent" field will be
- # removed from each device. If it is enabled, then the field name will
- # be replaced by the unstable identifier.
- #
- # When MSC3852 is accepted, this block of code can just be removed to
- # expose "last_seen_user_agent" to clients.
- last_seen_user_agent = device["last_seen_user_agent"]
+ # This field is only for admin access and should not be exposed to clients.
+ # (MSC3852, which is closed, did propose to expose it.)
del device["last_seen_user_agent"]
- if self._msc3852_enabled:
- device["org.matrix.msc3852.last_seen_user_agent"] = last_seen_user_agent
return 200, device