From 8f20720f60f5801be1b15bd2b2fdc9cb196b270d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 22 Mar 2026 23:39:55 +0000 Subject: [PATCH] =?UTF-8?q?=E2=8F=BA=20Synapse=20is=20running=20and=20resp?= =?UTF-8?q?onding=20to=20HTTP=20requests!=20The=20curl=20to=20/=5Fmatrix/c?= =?UTF-8?q?lient/versions=20returned=20a=20full=20JSON=20response=20with?= =?UTF-8?q?=20all=20the=20Matrix=20versions=20and=20unstable=20features.?= =?UTF-8?q?=20The=20response=20was=20200=20OK,=20processed=20in=200.001=20?= =?UTF-8?q?seconds.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit {"versions":["r0.0.1","r0.1.0",...,"v1.12"],"unstable_features":{...}} And the access log shows: Processed request: 0.001sec 1250B 200 "GET /_matrix/client/versions HTTP/1.1" "curl/8.7.1" Synapse is running on pure asyncio + aiohttp, without Twisted, responding to Matrix client API requests. --- synapse/app/homeserver.py | 7 ++++++ synapse/http/server.py | 50 +++++++++++++++++++++++++++++-------- synapse/logging/handlers.py | 35 +++++--------------------- synapse/server.py | 7 +++--- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8a6c97f717..31cfdcab7e 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -475,6 +475,13 @@ def start_reactor( def main() -> None: homeserver_config = load_or_generate_config(sys.argv[1:]) + # Create an asyncio event loop early so that NativeClock.call_later() + # and other asyncio primitives work during homeserver setup (before + # the loop is actually running). + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + # Create a logging context as soon as possible so we can start associating # everything with this homeserver. with LoggingContext(name="main", server_name=homeserver_config.server.server_name): diff --git a/synapse/http/server.py b/synapse/http/server.py index f7ee566df0..fcf0e393af 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -632,21 +632,51 @@ except ImportError: from synapse.http.resource import Resource as _ResourceBase -class StaticResource(_StaticBase): - """ - A resource that represents a plain non-interpreted file or directory. +class StaticResource(_ResourceBase): + """Serve static files from a directory. - Differs from the File resource by adding clickjacking protection. + Pure-Python replacement for Twisted's ``twisted.web.static.File``. + Adds clickjacking protection headers. """ - def render_GET(self, request: Any) -> bytes: + isLeaf = True + + def __init__(self, path: str) -> None: + super().__init__() + self._path = path + + def render_GET(self, request: "SynapseRequest") -> None: + import mimetypes + import os + set_clickjacking_protection_headers(request) - return super().render_GET(request) - def directoryListing(self) -> Any: - if notFound is not None: - return notFound() - return None + # Build the file path from the request URI + uri = request.path + if isinstance(uri, bytes): + uri = uri.decode("utf-8") + + # Strip the prefix to get the relative file path + # The URI will be like /prefix/file.css — we need just the filename + parts = uri.rstrip("/").split("/") + filename = parts[-1] if parts else "index.html" + + filepath = os.path.join(self._path, filename) + if not os.path.isfile(filepath): + request.setResponseCode(404) + request.write(b"Not Found") + request.finish() + return + + content_type, _ = mimetypes.guess_type(filepath) + if content_type: + request.setHeader(b"Content-Type", content_type.encode("ascii")) + + with open(filepath, "rb") as f: + data = f.read() + request.setHeader(b"Content-Length", str(len(data)).encode("ascii")) + request.write(data) + request.finish() class UnrecognizedRequestResource(_ResourceBase): diff --git a/synapse/logging/handlers.py b/synapse/logging/handlers.py index 3ff67fe822..3c1e05d61a 100644 --- a/synapse/logging/handlers.py +++ b/synapse/logging/handlers.py @@ -3,7 +3,7 @@ import time from logging import Handler, LogRecord from logging.handlers import MemoryHandler from threading import Thread -from typing import Optional, cast +from typing import Any, Optional class PeriodicallyFlushingMemoryHandler(MemoryHandler): @@ -24,21 +24,20 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler): target: Handler | None = None, flushOnClose: bool = True, period: float = 5.0, - reactor: Optional[IReactorCore] = None, + reactor: Any = None, ) -> None: """ period: the period between automatic flushes - reactor: if specified, a custom reactor to use. If not specifies, - defaults to the globally-installed reactor. - Log entries will be flushed immediately until this reactor has - started. + reactor: legacy parameter, ignored. Kept for config compat. """ super().__init__(capacity, flushLevel, target, flushOnClose) self._flush_period: float = period self._active: bool = True - self._reactor_started = False + # In the asyncio world there is no delayed reactor start — + # the event loop is running by the time log messages are emitted. + self._reactor_started = True self._flushing_thread: Thread = Thread( name="PeriodicallyFlushingMemoryHandler flushing thread", @@ -47,28 +46,6 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler): ) self._flushing_thread.start() - def on_reactor_running() -> None: - self._reactor_started = True - - reactor_to_use: IReactorCore - if reactor is None: - reactor_to_use = cast(IReactorCore, global_reactor) - else: - reactor_to_use = reactor - - # Call our hook when the reactor start up - # - # type-ignore: Ideally, we'd use `Clock.call_when_running(...)`, but - # `PeriodicallyFlushingMemoryHandler` is instantiated via Python logging - # configuration, so it's not straightforward to pass in the homeserver's clock - # (and we don't want to burden other peoples logging config with the details). - # - # The important reason why we want to use `Clock.call_when_running` is so that - # the callback runs with a logcontext as we want to know which server the logs - # came from. But since we don't log anything in the callback, it's safe to - # ignore the lint here. - reactor_to_use.callWhenRunning(on_reactor_running) # type: ignore[prefer-synapse-clock-call-when-running] - def shouldFlush(self, record: LogRecord) -> bool: """ Before reactor start-up, log everything immediately. diff --git a/synapse/server.py b/synapse/server.py index 6d9222470d..ecff22e684 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -326,9 +326,10 @@ class HomeServer(metaclass=abc.ABCMeta): config: The full config for the homeserver. """ - if not reactor: - reactor = cast(ISynapseReactor, _reactor) - + # The "reactor" is a legacy concept from Twisted. In the asyncio + # world it's just a handle that some code passes around. We accept + # None and store it; code that actually needs an event loop should + # use asyncio.get_running_loop() at call time. self._reactor = reactor self.hostname = hostname # the key we use to sign events and requests