Repository: matrix-org/synapse

Issue:
Synapse crashes with `AttributeError: 'NoneType' object has no attribute 'get'` on `.get("msc3861", {})` if `experimental_features` is empty
### Description

Since upgrading from 1.85.2 to 1.87.0 just now, Synapse refuses to start. After quickly looking at the source code it turns out it like/expect when an empty `experimental_features` appears in the configuration:

```
# Configure the forbidden fruits
#
# See https://github.com/matrix-org/synapse/blob/master/synapse/config/experimental.py for current list.
experimental_features:
```

Commenting out that last line fixes the issue and makes Synapse start again. This happened because that key previous contained some entries but they where later removed as the relevant feature became stable. Perhaps it shouldn’t have been written this way before, but Synapse failing like this on upgrade feels like a regression.

### Steps to reproduce

- list the steps
- that reproduce the bug
- using hyphens as bullet points


### Homeserver

matrix.ninetailed.nnja

### Synapse Version

187.0

### Installation Method

Docker (matrixdotorg/synapse)

### Database

PostgreSQL, single server, never restored from backup or SQLite

### Workers

Single process

### Platform

Linux x86_64, Docker container inside VM

### Configuration

_No response_

### Relevant log output

```shell
matrix-synapse        | Starting synapse with args -m synapse.app.homeserver --config-path /data/homeserver.yaml
matrix-synapse        | Traceback (most recent call last):
matrix-synapse        |   File "<frozen runpy>", line 198, in _run_module_as_main
matrix-synapse        |   File "<frozen runpy>", line 88, in _run_code
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/app/homeserver.py", line 398, in <module>
matrix-synapse        |     main()
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/app/homeserver.py", line 388, in main
matrix-synapse        |     hs = setup(sys.argv[1:])
matrix-synapse        |          ^^^^^^^^^^^^^^^^^^^
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/app/homeserver.py", line 297, in setup
matrix-synapse        |     config = HomeServerConfig.load_or_generate_config(
matrix-synapse        |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/config/_base.py", line 802, in load_or_generate_config
matrix-synapse        |     obj.parse_config_dict(
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/config/_base.py", line 823, in parse_config_dict
matrix-synapse        |     self.invoke_all(
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/config/_base.py", line 422, in invoke_all
matrix-synapse        |     res[config_class.section] = getattr(config, func_name)(*args, **kwargs)
matrix-synapse        |                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
matrix-synapse        |   File "/usr/local/lib/python3.11/site-packages/synapse/config/auth.py", line 35, in read_config
matrix-synapse        |     .get("msc3861", {})
matrix-synapse        |      ^^^
matrix-synapse        | AttributeError: 'NoneType' object has no attribute 'get'
```


### Anything else that would be useful to know?

_No response_


Discussion:
Diagnosed it now: As mentioned in the description now, it turns out it like/expect when an empty `experimental_features` key appears in the configuration.
We should check if it is a dict before that call.

Resolving patch:
diff --git a/synapse/config/auth.py b/synapse/config/auth.py
--- a/synapse/config/auth.py
+++ b/synapse/config/auth.py
@@ -31,7 +31,7 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
 
         # The default value of password_config.enabled is True, unless msc3861 is enabled.
         msc3861_enabled = (
-            config.get("experimental_features", {})
+            (config.get("experimental_features") or {})
             .get("msc3861", {})
             .get("enabled", False)
         )


Test patch: