Skip to content

Don't mask open errors in the filesystem transport - #2683

Merged
auvipy merged 5 commits into
celery:mainfrom
kratos0718:fix-filesystem-open-error-masked
Sep 30, 2026
Merged

auvipy merged 5 commits into
celery:mainfrom
kratos0718:fix-filesystem-open-error-masked

Conversation

@kratos0718

Copy link
Copy Markdown
Contributor

Channel._put() and Channel._queue_bind() in the filesystem transport open their file inside a try whose finally unconditionally unlocks and closes it. If open() fails, the finally raises UnboundLocalError and hides the real error. For _put() that also swallows the ChannelError('Cannot add file ...') it is meant to raise. A data_folder_out that does not exist is enough to hit it:

  File "kombu/transport/filesystem.py", line 272, in _put
    unlock(f)
UnboundLocalError: cannot access local variable 'f' where it is not associated with a value

This initialises the handle to None and only unlocks and closes it once it was opened, the same way _delete() already does. I added two tests in t/unit/transport/test_filesystem.py that fail on main with the UnboundLocalError and pass with the change. The unit suite and pre-commit pass.

_put() and _queue_bind() opened their file inside a try whose finally
unconditionally unlocked and closed it. When open() failed, for example
because data_folder_out does not exist, the finally raised
UnboundLocalError and hid the real error, including the ChannelError
_put() means to raise.

Only unlock and close the file once it was opened, as _delete() already
does.
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.06%. Comparing base (681cca5) to head (5f42611).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
kombu/transport/filesystem.py 87.50% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2683      +/-   ##
==========================================
+ Coverage   85.02%   85.06%   +0.04%     
==========================================
  Files          80       80              
  Lines       11317    11322       +5     
  Branches     1354     1356       +2     
==========================================
+ Hits         9622     9631       +9     
+ Misses       1475     1471       -4     
  Partials      220      220              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@auvipy
auvipy self-requested a review September 27, 2026 15:32
@auvipy auvipy added this to the 5.7.0 milestone Sep 27, 2026

@MehrazRumman MehrazRumman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kratos0718, small and correct.

  • In _put() and _queue_bind(), a failing open() left f / f_obj unbound, so the finally raised UnboundLocalError and hid the real error. For _put() that also meant the intended ChannelError never reached the caller. Initialising to None and only unlocking/closing when the handle exists matches what _delete() already does.
  • The tests reproduce both paths realistically (removing data_folder_out; putting a directory where the exchange file should be) rather than mocking open, and both would fail on main with UnboundLocalError.

Minor, non-blocking:

  • Inconsistent error types. After this change, an open failure in _put() becomes ChannelError, while the same failure in _queue_bind() propagates as a raw OSError (and get_table() converts OSError to ChannelError). Since ChannelError is in the transport's channel_errors, which callers such as Celery know how to handle, wrapping _queue_bind()'s open failure the same way would make the transport consistent. Either way, the test name test_queue_bind_propagates_open_error documents the current behaviour.
  • Chaining. raise ChannelError(f'Cannot add file {filename!r} to directory') inside except OSError: has no from exc, so the message doesn't say why (permission denied, missing directory, ...). The original error is still attached as __context__, but raise ... from exc (or including exc.strerror in the message) would make logs clearer.
  • A similar leak nearby (pre-existing). In _get(), f = open(...); payload = f.read(); f.close() leaks the handle if read() fails. A with open(...) as f: would fix it if you want to tidy that up in the same spirit, but it's fine as a follow-up.

LGTM.

@auvipy

auvipy commented Sep 29, 2026

Copy link
Copy Markdown
Member
  • Inconsistent error types. After this change, an open failure in _put() becomes ChannelError, while the same failure in _queue_bind() propagates as a raw OSError (and get_table() converts OSError to ChannelError). Since ChannelError is in the transport's channel_errors, which callers such as Celery know how to handle, wrapping _queue_bind()'s open failure the same way would make the transport consistent. Either way, the test name test_queue_bind_propagates_open_error documents the current behaviour.

  • Chaining. raise ChannelError(f'Cannot add file {filename!r} to directory') inside except OSError: has no from exc, so the message doesn't say why (permission denied, missing directory, ...). The original error is still attached as __context__, but raise ... from exc (or including exc.strerror in the message) would make logs clearer.

  • A similar leak nearby (pre-existing). In _get(), f = open(...); payload = f.read(); f.close() leaks the handle if read() fails. A with open(...) as f: would fix it if you want to tidy that up in the same spirit

these should be fixed in this pr for hardening this @kratos0718

auvipy and others added 2 commits September 29, 2026 23:53
…e in _get

- _queue_bind now wraps a failure to open the exchange file in
  ChannelError like _put and get_table, so callers get the transport's
  channel error in every case.
- every ChannelError raised for an OSError now chains it with 'from exc',
  so the log says why the file could not be opened.
- _get reads the message file in a with block, so the handle is closed
  when read() fails.
@kratos0718

Copy link
Copy Markdown
Contributor Author

Done in the latest commit. _queue_bind now raises ChannelError for an open failure like _put and get_table, every ChannelError raised for an OSError chains it with from exc, and _get reads the message file in a with block. The tests check the chained cause for both open paths, and a new one checks that _get closes the file when read() fails.

@auvipy
auvipy merged commit e66220c into celery:main Sep 30, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants