codecov[bot]: # [Codecov](https://codecov.io/gh/matrix-org/synapse/pull/5593?src=pr&el=h1) Report
> Merging [#5593](https://codecov.io/gh/matrix-org/synapse/pull/5593?src=pr&el=desc) into [release-v1.1.0](https://codecov.io/gh/matrix-org/synapse/commit/f8b52eb8c5fc4c723fad49df7a5339c1239b34be?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `76.92%`.

```diff
@@                Coverage Diff                 @@
##           release-v1.1.0    #5593      +/-   ##
==================================================
- Coverage           63.16%   63.14%   -0.02%     
==================================================
  Files                 328      328              
  Lines               35930    35939       +9     
  Branches             5917     5920       +3     
==================================================
  Hits                22694    22694              
- Misses              11609    11614       +5     
- Partials             1627     1631       +4
```

diff --git a/changelog.d/5593.bugfix b/changelog.d/5593.bugfix
new file mode 100644
index 000000000..e981589ac
--- /dev/null
+++ b/changelog.d/5593.bugfix
@@ -0,0 +1,1 @@
+Fix regression in 1.1rc1 where OPTIONS requests to the media repo would fail.
diff --git a/synapse/http/server.py b/synapse/http/server.py
index f067c163c..d993161a3 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -65,8 +65,8 @@ def wrap_json_request_handler(h):
     The handler method must have a signature of "handle_foo(self, request)",
     where "request" must be a SynapseRequest.
 
-    The handler must return a deferred. If the deferred succeeds we assume that
-    a response has been sent. If the deferred fails with a SynapseError we use
+    The handler must return a deferred or a coroutine. If the deferred succeeds
+    we assume that a response has been sent. If the deferred fails with a SynapseError we use
     it to send a JSON response with the appropriate HTTP reponse code. If the
     deferred fails with any other type of error we send a 500 reponse.
     """
@@ -353,16 +353,22 @@ class DirectServeResource(resource.Resource):
         """
         Render the request, using an asynchronous render handler if it exists.
         """
-        render_callback_name = "_async_render_" + request.method.decode("ascii")
+        async_render_callback_name = "_async_render_" + request.method.decode("ascii")
 
-        if hasattr(self, render_callback_name):
-            # Call the handler
-            callback = getattr(self, render_callback_name)
-            defer.ensureDeferred(callback(request))
+        # Try and get the async renderer
+        callback = getattr(self, async_render_callback_name, None)
 
-            return NOT_DONE_YET
-        else:
-            super().render(request)
+        # No async renderer for this request method.
+        if not callback:
+            return super().render(request)
+
+        resp = callback(request)
+
+        # If it's a coroutine, turn it into a Deferred
+        if isinstance(resp, types.CoroutineType):
+            defer.ensureDeferred(resp)
+
+        return NOT_DONE_YET
 
 
 def _options_handler(request):
diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py
index 0337b64dc..053346fb8 100644
--- a/synapse/rest/media/v1/preview_url_resource.py
+++ b/synapse/rest/media/v1/preview_url_resource.py
@@ -95,6 +95,7 @@ class PreviewUrlResource(DirectServeResource):
         )
 
     def render_OPTIONS(self, request):
+        request.setHeader(b"Allow", b"OPTIONS, GET")
         return respond_with_json(request, 200, {}, send_cors=True)
 
     @wrap_json_request_handler
diff --git a/synapse/util/logcontext.py b/synapse/util/logcontext.py
index 6b0d2deea..9e1b53780 100644
--- a/synapse/util/logcontext.py
+++ b/synapse/util/logcontext.py
@@ -24,6 +24,7 @@ See doc/log_contexts.rst for details on how this works.
 
 import logging
 import threading
+import types
 
 from twisted.internet import defer, threads
 
@@ -528,8 +529,9 @@ def run_in_background(f, *args, **kwargs):
     return from the function, and that the sentinel context is set once the
     deferred returned by the function completes.
 
-    Useful for wrapping functions that return a deferred which you don't yield
-    on (for instance because you want to pass it to deferred.gatherResults()).
+    Useful for wrapping functions that return a deferred or coroutine, which you don't
+    yield or await on (for instance because you want to pass it to
+    deferred.gatherResults()).
 
     Note that if you completely discard the result, you should make sure that
     `f` doesn't raise any deferred exceptions, otherwise a scary-looking
@@ -544,6 +546,9 @@ def run_in_background(f, *args, **kwargs):
         # by synchronous exceptions, so let's turn them into Failures.
         return defer.fail()
 
+    if isinstance(res, types.CoroutineType):
+        res = defer.ensureDeferred(res)
+
     if not isinstance(res, defer.Deferred):
         return res
 

media upload is broken
It gives a 500:

```
2019-07-02 12:33:34,534 - synapse.access.http.8008 - 233 - INFO - OPTIONS-12451- 2001:8b0:ba7a:e98f:74ca:71dd:a262:d20e - 8008 - Received request: OPTIONS /_matrix/media/v1/upload
2019-07-02 12:33:34,535 - twisted - 253 - CRITICAL - OPTIONS-12451- 
RuntimeError: Producer was not unregistered for b'/_matrix/media/v1/upload'
2019-07-02 12:33:34,535 - synapse.access.http.8008 - 302 - INFO - OPTIONS-12451- 2001:8b0:ba7a:e98f:74ca:71dd:a262:d20e - 8008 - {None} Processed request: 0.001sec/-0.001sec (0.000sec, 0.000sec) (0.000sec/0.000sec/0) 471B 500 "OPTIONS /_matrix/media/v1/upload HTTP/1.1" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 
2019-07-02 12:33:34,637 - synapse.access.http.8008 - 233 - INFO - OPTIONS-12452- 2001:8b0:ba7a:e98f:74ca:71dd:a262:d20e - 8008 - Received request: OPTIONS /_matrix/media/v1/upload?filename=cat_confused.gif
2019-07-02 12:33:34,638 - twisted - 253 - CRITICAL - OPTIONS-12452- 
RuntimeError: Producer was not unregistered for b'/_matrix/media/v1/upload?filename=cat_confused.gif'
2019-07-02 12:33:34,638 - synapse.access.http.8008 - 302 - INFO - OPTIONS-12452- 2001:8b0:ba7a:e98f:74ca:71dd:a262:d20e - 8008 - {None} Processed request: 0.001sec/-0.000sec (0.000sec, 0.000sec) (0.000sec/0.000sec/0) 497B 500 "OPTIONS /_matrix/media/v1/upload?filename=cat_confused.gif HTTP/1.1" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Ge
```
diff --git a/tests/rest/media/v1/test_url_preview.py b/tests/rest/media/v1/test_url_preview.py
index 8fe596186..976652aee 100644
--- a/tests/rest/media/v1/test_url_preview.py
+++ b/tests/rest/media/v1/test_url_preview.py
@@ -460,3 +460,15 @@ class URLPreviewTests(unittest.HomeserverTestCase):
                 "error": "DNS resolution failure during URL preview generation",
             },
         )
+
+    def test_OPTIONS(self):
+        """
+        OPTIONS returns the OPTIONS.
+        """
+        request, channel = self.make_request(
+            "OPTIONS", "url_preview?url=http://example.com", shorthand=False
+        )
+        request.render(self.preview_url)
+        self.pump()
+        self.assertEqual(channel.code, 200)
+        self.assertEqual(channel.json_body, {})
diff --git a/tests/util/test_logcontext.py b/tests/util/test_logcontext.py
index 8adaee3c8..8d69fbf11 100644
--- a/tests/util/test_logcontext.py
+++ b/tests/util/test_logcontext.py
@@ -39,24 +39,17 @@ class LoggingContextTestCase(unittest.TestCase):
 
         callback_completed = [False]
 
-        def test():
+        with LoggingContext() as context_one:
             context_one.request = "one"
-            d = function()
+
+            # fire off function, but don't wait on it.
+            d2 = logcontext.run_in_background(function)
 
             def cb(res):
-                self._check_test_key("one")
                 callback_completed[0] = True
                 return res
 
-            d.addCallback(cb)
-
-            return d
-
-        with LoggingContext() as context_one:
-            context_one.request = "one"
-
-            # fire off function, but don't wait on it.
-            logcontext.run_in_background(test)
+            d2.addCallback(cb)
 
             self._check_test_key("one")
 
@@ -105,6 +98,22 @@ class LoggingContextTestCase(unittest.TestCase):
 
         return self._test_run_in_background(testfunc)
 
+    def test_run_in_background_with_coroutine(self):
+        async def testfunc():
+            self._check_test_key("one")
+            d = Clock(reactor).sleep(0)
+            self.assertIs(LoggingContext.current_context(), LoggingContext.sentinel)
+            await d
+            self._check_test_key("one")
+
+        return self._test_run_in_background(testfunc)
+
+    def test_run_in_background_with_nonblocking_coroutine(self):
+        async def testfunc():
+            self._check_test_key("one")
+
+        return self._test_run_in_background(testfunc)
+
     @defer.inlineCallbacks
     def test_make_deferred_yieldable(self):
         # a function which retuns an incomplete deferred, but doesn't follow

pip install -e .[all]
parse_log_pytest
pytest
pytest coverage coverage-enable-subprocess
apt-get update apt-get install -y build-essential python3-dev libffi-dev libssl-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
acme==0.40.1
aiocontextvars==0.2.2
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Automat==22.10.0
bcrypt==4.0.1
bleach==4.1.0
boltons==23.1.1
canonicaljson==1.5.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
constantly==15.1.0
contextvars==2.4
coverage==6.2
coverage-enable-subprocess==1.0
cryptography==40.0.2
daemonize==2.5.0
defusedxml==0.7.1
elementpath==2.4.0
eliot==1.14.0
frozendict==2.4.6
hyperlink==21.0.0
idna==3.10
immutables==0.19
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
importlib-resources==5.4.0
incremental==22.10.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.0.3
josepy==1.13.0
jsonschema==3.2.0
ldap3==2.9.1
lxml==5.3.1
MarkupSafe==2.0.1
-e git+https://github.com/matrix-org/synapse.git@f8b52eb8c5fc4c723fad49df7a5339c1239b34be#egg=matrix_synapse
matrix-synapse-ldap3==0.1.5
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
msgpack==1.0.5
netaddr==0.10.1
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
parameterized==0.8.1
pem==21.2.0
phonenumbers==9.0.2
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
prometheus-client==0.3.1
psutil==7.0.0
psycopg2==2.7.7
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
PyJWT==2.4.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyOpenSSL==23.2.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyRFC3339==2.0.1
pyrsistent==0.18.0
pysaml2==7.3.1
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
requests==2.27.1
requests-toolbelt==1.0.0
sentry-sdk==2.24.1
service-identity==21.1.0
signedjson==1.1.4
simplejson==3.20.1
six==1.17.0
sortedcontainers==2.4.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
treq==22.2.0
Twisted==22.4.0
txacme==0.9.3
TxSNI==0.2.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
unpaddedbase64==2.1.0
urllib3==1.26.20
webencodings==0.5.1
xmlschema==1.9.2
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.interface==5.5.2

name: synapse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - acme==0.40.1
      - aiocontextvars==0.2.2
      - automat==22.10.0
      - bcrypt==4.0.1
      - bleach==4.1.0
      - boltons==23.1.1
      - canonicaljson==1.5.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - constantly==15.1.0
      - contextvars==2.4
      - coverage==6.2
      - coverage-enable-subprocess==1.0
      - cryptography==40.0.2
      - daemonize==2.5.0
      - defusedxml==0.7.1
      - elementpath==2.4.0
      - eliot==1.14.0
      - frozendict==2.4.6
      - hyperlink==21.0.0
      - idna==3.10
      - immutables==0.19
      - importlib-resources==5.4.0
      - incremental==22.10.0
      - jinja2==3.0.3
      - josepy==1.13.0
      - jsonschema==3.2.0
      - ldap3==2.9.1
      - lxml==5.3.1
      - markupsafe==2.0.1
      - matrix-synapse-ldap3==0.1.5
      - mock==5.2.0
      - msgpack==1.0.5
      - netaddr==0.10.1
      - parameterized==0.8.1
      - pem==21.2.0
      - phonenumbers==9.0.2
      - pillow==8.4.0
      - prometheus-client==0.3.1
      - psutil==7.0.0
      - psycopg2==2.7.7
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycparser==2.21
      - pyjwt==2.4.0
      - pymacaroons==0.13.0
      - pynacl==1.5.0
      - pyopenssl==23.2.0
      - pyrfc3339==2.0.1
      - pyrsistent==0.18.0
      - pysaml2==7.3.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - sentry-sdk==2.24.1
      - service-identity==21.1.0
      - signedjson==1.1.4
      - simplejson==3.20.1
      - six==1.17.0
      - sortedcontainers==2.4.0
      - treq==22.2.0
      - twisted==22.4.0
      - txacme==0.9.3
      - txsni==0.2.0
      - unpaddedbase64==2.1.0
      - urllib3==1.26.20
      - webencodings==0.5.1
      - xmlschema==1.9.2
      - zope-interface==5.5.2
prefix: /opt/conda/envs/synapse

tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_OPTIONS tests/util/test_logcontext.py::LoggingContextTestCase::test_run_in_background_with_coroutine tests/util/test_logcontext.py::LoggingContextTestCase::test_run_in_background_with_nonblocking_coroutine
tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_range tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_range_direct tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_range_whitelisted_ip tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_specific tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_specific_direct tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ip_with_external_ip tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ipv6_range tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_blacklisted_ipv6_specific tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_cache_returns_correct_type tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_ipaddr tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_non_ascii_preview_content_type tests/rest/media/v1/test_url_preview.py::URLPreviewTests::test_non_ascii_preview_httpequiv tests/util/test_logcontext.py::LoggingContextTestCase::test_make_deferred_yieldable tests/util/test_logcontext.py::LoggingContextTestCase::test_make_deferred_yieldable_on_non_deferred tests/util/test_logcontext.py::LoggingContextTestCase::test_make_deferred_yieldable_with_chained_deferreds tests/util/test_logcontext.py::LoggingContextTestCase::test_nested_logging_context tests/util/test_logcontext.py::LoggingContextTestCase::test_run_in_background_with_blocking_fn tests/util/test_logcontext.py::LoggingContextTestCase::test_run_in_background_with_chained_deferred tests/util/test_logcontext.py::LoggingContextTestCase::test_run_in_background_with_non_blocking_fn tests/util/test_logcontext.py::LoggingContextTestCase::test_sleep tests/util/test_logcontext.py::LoggingContextTestCase::test_with_context
synapse/http/server.py changelog.d/5593.bugfix synapse/rest/media/v1/preview_url_resource.py synapse/util/logcontext.py
synapse/http/server.py changelog.d/5593.bugfix synapse/rest/media/v1/preview_url_resource.py synapse/util/logcontext.py