33#
44# Copyright 2021 The Matrix.org Foundation C.I.C.
55# Copyright (C) 2023 New Vector, Ltd
6+ # Copyright (C) 2026 Element Creations Ltd.
67#
78# This program is free software: you can redistribute it and/or modify
89# it under the terms of the GNU Affero General Public License as
1516# Originally licensed under the Apache License, Version 2.0:
1617# <http://www.apache.org/licenses/LICENSE-2.0>.
1718#
18- # [This file includes modifications made by New Vector Limited]
19+ # [This file includes modifications made by New Vector Limited
20+ # and Element Creations Ltd]
1921#
2022#
2123
@@ -234,9 +236,9 @@ async def non_caching(o: str, cache_context: ResponseCacheContext[int]) -> str:
234236 [], cache .keys (), "cache should not have the result now"
235237 )
236238
237- def test_cache_func_errors (self ) -> None :
239+ def test_errors_raised_to_all_waiters (self ) -> None :
238240 """If the callback raises an error, the error should be raised to all
239- callers and the result should not be cached """
241+ concurrent callers that were waiting on the same in-flight result. """
240242 cache = self .with_cache ("error_cache" , ms = 3000 )
241243
242244 expected_error = Exception ("oh no" )
@@ -259,6 +261,60 @@ async def erring(o: str) -> str:
259261 self .assertFailure (wrap_d , Exception )
260262 self .assertFailure (wrap2_d , Exception )
261263
264+ def test_errors_are_not_cached (self ) -> None :
265+ """If the callback raises an error, the error is not cached and
266+ served to any subsequent requests.
267+ """
268+ cache = self .with_cache ("error_not_cached" , ms = 3000 )
269+
270+ return_error = True
271+
272+ REQUEST_FAKE_WORK_SLEEP_TIME = Duration (seconds = 1 )
273+
274+ async def erring_then_fine (_ : str ) -> str :
275+ """
276+ This function raises an error the first time it is called,
277+ then is fine the next time it is called.
278+ """
279+ nonlocal return_error
280+
281+ # pretend to do some work
282+ await self .clock .sleep (REQUEST_FAKE_WORK_SLEEP_TIME )
283+
284+ if return_error :
285+ return_error = False
286+ raise RuntimeError ("this is a temporary error!" )
287+ return "fine"
288+
289+ # First call: should get an error
290+ wrap_d = defer .ensureDeferred (cache .wrap (0 , erring_then_fine , "ignored" ))
291+
292+ # Should be pending
293+ self .assertNoResult (wrap_d )
294+
295+ # Wait for the time it takes for the request to resolve to an error
296+ self .reactor .advance (REQUEST_FAKE_WORK_SLEEP_TIME .as_secs ())
297+
298+ # Check that the Deferred resolved to an error of the correct type
299+ self .failureResultOf (wrap_d , RuntimeError )
300+
301+ # Second call: the error shouldn't be replayed
302+ # and the next call should succeed, so we should get a successful response
303+ wrap2_d = defer .ensureDeferred (cache .wrap (0 , erring_then_fine , "ignored" ))
304+
305+ # Since this is a new request (not coalesced with the previous failed one),
306+ # this should still be pending
307+ self .assertNoResult (wrap2_d )
308+
309+ # Wait for the time it takes for the request to resolve
310+ self .reactor .advance (REQUEST_FAKE_WORK_SLEEP_TIME .as_secs ())
311+
312+ self .assertEqual (
313+ self .successResultOf (wrap2_d ),
314+ "fine" ,
315+ "should get the fresh result, not the cached error" ,
316+ )
317+
262318 def test_cache_cancel_first_wait (self ) -> None :
263319 """Test that cancellation of the deferred returned by wrap() on the
264320 first call does not immediately cause a cancellation error to be raised
0 commit comments