Describe the bug
TMDB indexer calls (medusa/indexers/tmdb/api.py) can fail with a confusing AttributeError: 'NoneType' object has no attribute 'raise_for_status' instead of a proper, informative network error. Root cause is a type-contract mismatch between two of Medusa's own layers:
Tmdb.__init__ injects Medusa's own session into the vendored third-party tmdbsimple library: self.tmdb.REQUESTS_SESSION = self.config['session'] — and that session (IndexerSession, from medusa/session/core.py) subclasses MedusaSafeSession.
MedusaSafeSession.request() is explicitly designed to swallow exceptions and return None on failure (see the bare except Exception branch at the bottom of MedusaSafeSession.request() — logs at INFO ("Unknown exception...") with the real traceback only at DEBUG, then return None).
tmdbsimple's base.py::_request() (ext/tmdbsimple/base.py line 105) has no such contract in mind — it assumes the session's .request() always either returns a real requests.Response or raises, so it unconditionally calls response.raise_for_status(). When MedusaSafeSession hands back None, this blows up as AttributeError: 'NoneType' object has no attribute 'raise_for_status'.
Net effect: whatever the actual underlying network problem was (timeout, connection reset, DNS blip — anything landing in that generic except Exception branch) is completely hidden. The log shows a Python AttributeError deep inside a third-party dependency instead of the real cause, which is only visible if debug logging happens to be enabled. Every call site in tmdb/api.py that goes through _GET/_request (e.g. _get_all_updates, used by get_last_updated_series, and the external-ID lookup in medusa/helpers/externals.py) is exposed to this, though in the cases I hit it was caught by a surrounding try/except a level up and converted into a merely-unhelpful IndexerException rather than crashing the app.
To Reproduce
Hard to force on demand (depends on hitting a transient network hiccup at the exact moment MedusaSafeSession.request() catches something), but the mechanism is fully reproducible by inspection:
Tmdb.__init__ sets tmdbsimple.REQUESTS_SESSION to a MedusaSafeSession-derived IndexerSession.
- Any transient error that lands in
MedusaSafeSession.request()'s final except Exception branch returns None instead of raising.
tmdbsimple.base.TMDB._request() calls response.raise_for_status() unconditionally → AttributeError.
In practice, I saw this hit real, live traffic: on 2026-07-30 at 04:45 UTC, the daily ShowUpdater run's TMDB "get last updated series" call failed this way for every TMDB-indexed show in my library (6/6), each logging the same opaque AttributeError traceback instead of whatever the real network issue was.
Expected behavior
MedusaSafeSession's "return None on failure" contract should not be handed to third-party libraries (like vendored tmdbsimple) that don't expect it — either use a session type for the TMDB indexer that raises normally (matching what tmdbsimple assumes), or have tmdb/api.py check for a None/falsy response before letting tmdbsimple touch it, or patch the vendored tmdbsimple/base.py::_request() to guard if response is None: raise ... with a clear message. Any of these would replace the current opaque AttributeError with the real, actionable error.
Suggested fix
Simplest targeted fix: in medusa/indexers/tmdb/api.py::Tmdb.__init__, don't hand tmdbsimple a MedusaSafeSession-derived session — either construct a plain requests.Session/MedusaSession (the non-"safe" base class, which doesn't swallow exceptions) for this purpose, or wrap the assignment so a None return is converted back into a raised RequestException before tmdbsimple sees it.
Medusa (please complete the following information):
- OS: Debian GNU/Linux 12 (bookworm)
- Branch: master
- Commit: 2bc1eb2
- Python version: 3.11.2
- Database version: 44.19
Debug logs (at least 50 lines):
Details
2026-07-30 04:45:21 WARNING SHOWUPDATER :: Problem running show_updater, Indexer TMDB seems to be having issues while trying to get updates for show The Westies. Cause: IndexerException("Could not get latest updates. Cause: 'NoneType' object has no attribute 'raise_for_status'")
Traceback (most recent call last):
File "medusa/indexers/tmdb/api.py", line 594, in _get_all_updates
updates = self.tmdb.Changes().tv(start_date=start_date, end_date=end_date, page=page)
File "ext/tmdbsimple/changes.py", line 72, in tv
response = self._GET(path, kwargs)
File "ext/tmdbsimple/base.py", line 110, in _GET
return self._request('GET', path, params=params)
File "ext/tmdbsimple/base.py", line 105, in _request
response.raise_for_status()
AttributeError: 'NoneType' object has no attribute 'raise_for_status'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "medusa/schedulers/show_updater.py", line 83, in run
indexer_updated_shows[show.indexer] = indexer_api.get_last_updated_series(
File "medusa/indexers/tmdb/api.py", line 626, in get_last_updated_series
total_updates += self._get_all_updates(search_from.strftime('%Y-%m-%d'), search_until.strftime('%Y-%m-%d'))
File "medusa/indexers/tmdb/api.py", line 601, in _get_all_updates
raise IndexerException('Could not get latest updates. Cause: {cause}'.format(
medusa.indexers.exceptions.IndexerException: Could not get latest updates. Cause: 'NoneType' object has no attribute 'raise_for_status'
Same shape repeated for The Paper, The Boys, IT: Welcome to Derry, High Potential, and Landman — every TMDB-indexed show in the library that run, all within the same minute.
Additional context
Found on the same instance/session as #12249, #12250, and #12252. The retry-storm behavior in show_updater.py (one failure causes every subsequent show for that indexer to re-attempt and re-fail the identical call) is already covered by #12252 and applies equally to TVDB and TMDB since it's the same code path — this issue is specifically about the opaque AttributeError masking the real cause, which is a separate defect in the TMDB indexer wrapper itself.
Describe the bug
TMDB indexer calls (
medusa/indexers/tmdb/api.py) can fail with a confusingAttributeError: 'NoneType' object has no attribute 'raise_for_status'instead of a proper, informative network error. Root cause is a type-contract mismatch between two of Medusa's own layers:Tmdb.__init__injects Medusa's own session into the vendored third-partytmdbsimplelibrary:self.tmdb.REQUESTS_SESSION = self.config['session']— and that session (IndexerSession, frommedusa/session/core.py) subclassesMedusaSafeSession.MedusaSafeSession.request()is explicitly designed to swallow exceptions and returnNoneon failure (see the bareexcept Exceptionbranch at the bottom ofMedusaSafeSession.request()— logs at INFO ("Unknown exception...") with the real traceback only at DEBUG, thenreturn None).tmdbsimple'sbase.py::_request()(ext/tmdbsimple/base.pyline 105) has no such contract in mind — it assumes the session's.request()always either returns a realrequests.Responseor raises, so it unconditionally callsresponse.raise_for_status(). WhenMedusaSafeSessionhands backNone, this blows up asAttributeError: 'NoneType' object has no attribute 'raise_for_status'.Net effect: whatever the actual underlying network problem was (timeout, connection reset, DNS blip — anything landing in that generic
except Exceptionbranch) is completely hidden. The log shows a PythonAttributeErrordeep inside a third-party dependency instead of the real cause, which is only visible if debug logging happens to be enabled. Every call site intmdb/api.pythat goes through_GET/_request(e.g._get_all_updates, used byget_last_updated_series, and the external-ID lookup inmedusa/helpers/externals.py) is exposed to this, though in the cases I hit it was caught by a surroundingtry/excepta level up and converted into a merely-unhelpfulIndexerExceptionrather than crashing the app.To Reproduce
Hard to force on demand (depends on hitting a transient network hiccup at the exact moment
MedusaSafeSession.request()catches something), but the mechanism is fully reproducible by inspection:Tmdb.__init__setstmdbsimple.REQUESTS_SESSIONto aMedusaSafeSession-derivedIndexerSession.MedusaSafeSession.request()'s finalexcept Exceptionbranch returnsNoneinstead of raising.tmdbsimple.base.TMDB._request()callsresponse.raise_for_status()unconditionally →AttributeError.In practice, I saw this hit real, live traffic: on 2026-07-30 at 04:45 UTC, the daily
ShowUpdaterrun's TMDB "get last updated series" call failed this way for every TMDB-indexed show in my library (6/6), each logging the same opaqueAttributeErrortraceback instead of whatever the real network issue was.Expected behavior
MedusaSafeSession's "returnNoneon failure" contract should not be handed to third-party libraries (like vendoredtmdbsimple) that don't expect it — either use a session type for the TMDB indexer that raises normally (matching whattmdbsimpleassumes), or havetmdb/api.pycheck for aNone/falsy response before lettingtmdbsimpletouch it, or patch the vendoredtmdbsimple/base.py::_request()to guardif response is None: raise ...with a clear message. Any of these would replace the current opaqueAttributeErrorwith the real, actionable error.Suggested fix
Simplest targeted fix: in
medusa/indexers/tmdb/api.py::Tmdb.__init__, don't handtmdbsimpleaMedusaSafeSession-derived session — either construct a plainrequests.Session/MedusaSession(the non-"safe" base class, which doesn't swallow exceptions) for this purpose, or wrap the assignment so aNonereturn is converted back into a raisedRequestExceptionbeforetmdbsimplesees it.Medusa (please complete the following information):
Debug logs (at least 50 lines):
Details
Same shape repeated for The Paper, The Boys, IT: Welcome to Derry, High Potential, and Landman — every TMDB-indexed show in the library that run, all within the same minute.
Additional context
Found on the same instance/session as #12249, #12250, and #12252. The retry-storm behavior in
show_updater.py(one failure causes every subsequent show for that indexer to re-attempt and re-fail the identical call) is already covered by #12252 and applies equally to TVDB and TMDB since it's the same code path — this issue is specifically about the opaqueAttributeErrormasking the real cause, which is a separate defect in the TMDB indexer wrapper itself.