Skip to content

DupFileManager, FileMonitor, RenameFile: bind the exception in retry handlers - #757

Open
johnkattenhorn wants to merge 1 commit into
stashapp:mainfrom
johnkattenhorn:fix/unbound-exception-variable
Open

DupFileManager, FileMonitor, RenameFile: bind the exception in retry handlers#757
johnkattenhorn wants to merge 1 commit into
stashapp:mainfrom
johnkattenhorn:fix/unbound-exception-variable

Conversation

@johnkattenhorn

Copy link
Copy Markdown

The problem

Each retry loop in StashPluginHelper.py has two exception handlers: a narrow one for connection errors, and a broad one for everything else. Both build the same log message including {e} — but only the broad handler binds the name with as e.

Because the sibling handler binds e as a function local, the narrow handler raises UnboundLocalError while formatting its own log message. That propagates out of the retry loop, so a dropped connection — the exact condition the loop exists to survive — aborts the operation on the first failure and reports the wrong error.

Reproduction, matching the structure in the source:

def retry():
    for i in range(3):
        try:
            raise ConnectionResetError("connection dropped")
        except (ConnectionResetError):
            print(f"retrying: {e}")     # UnboundLocalError
        except Exception as e:
            print(f"retrying: {e}")

Scope

13 sites across the three plugins that vendor StashPluginHelper.py:

File Lines
plugins/DupFileManager/StashPluginHelper.py 522, 713, 748, 769
plugins/FileMonitor/StashPluginHelper.py 522, 713, 748, 769
plugins/RenameFile/StashPluginHelper.py 522, 713, 748, 769
plugins/DupFileManager/DupFileManager.py 351

Those are the mergeMetadata, addTag, updateScene and destroyScene retry loops in each copy of the helper, plus setTagId_withRetry in DupFileManager.

Every change is the same one-token edit — adding as e to a handler that already interpolated e. No behaviour changes beyond the retry path now working as written.

Testing

Tested against a clean Stash v0.31.1 instance in Docker with a two-scene library, driving the plugin helper through the real plugin entry path (plugin JSON on stdin, server_connection context).

Fault injection: mergeMetadata was made to raise ConnectionResetError on its first two attempts, then succeed — the scenario the retry loop is written for. Same test run against both the current main and this branch:

RESULT[original]: UnboundLocalError: cannot access local variable 'e'
                  where it is not associated with a value   (attempts = 1)
RESULT[patched]:  survived the resets and completed;        attempts = 3

main aborts on the first reset. With the fix, the loop retries and completes as intended.

Also confirmed:

  • A normal (non-failing) mergeMetadata between two scenes behaves identically before and after — no regression in the success path.
  • AST pass over the whole repository: zero remaining except handlers that reference an unbound e.
  • All four modified files compile.

plugins/starIdentifier/star_identifier.py matches this pattern at first glance but is not affected — load_encodings() binds e = Exception(...) before the try, so its handler resolves the name from the enclosing scope. Deliberately left alone.

Note for maintainers

StashPluginHelper.py is vendored into three plugins and the copies have already drifted — DupFileManager's differs from the other two. This fix is applied to all three. Worth considering whether that file wants a single shared home.

LLM-assisted contribution disclosure

Per the repository's contribution policy: this change was prepared with LLM assistance (Claude). The diff has been reviewed by me, the testing described above was carried out and its results are reproduced verbatim, and I take full responsibility for the change and its license compliance.

…handlers

Each retry loop has two handlers: a narrow one for connection errors and a
broad one for everything else. Both format the same message including {e}, but
only the broad handler bound the name with "as e". The narrow handler raised
NameError while building its own log message, which propagated out of the retry
loop — so a dropped connection, the exact case the loop exists to survive,
aborted the operation and reported the wrong error.

Thirteen sites across the three plugins that vendor StashPluginHelper.py: the
mergeMetadata, addTag, updateScene and destroyScene retry loops in each copy,
plus setTagId_withRetry in DupFileManager.

Reproduces on any of them:

    try: raise ConnectionResetError("connection dropped")
    except (ConnectionResetError): print(f"{e}")   # NameError

Verified with an AST pass over the repository: zero remaining handlers that
reference an unbound e. starIdentifier's load_encodings looks like the same
pattern but is correct — it binds e before the try, so it is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant