Summary
When the LDAP server is unreachable, LDAPConnector.unbind() logs a full ERROR-level traceback ("Unbind not successful.") on teardown, even though the situation is handled. This pollutes logs with tracebacks on every connection failure.
Found while testing pas.plugins.ldap against node.ext.ldap 2.0.0 (Python 3.14, python-ldap 3.4.x).
Cause
In node/ext/ldap/base.py:
def unbind(self):
"""Unbind from Server."""
if self._con is None:
return
try:
self._con.unbind_s()
except AttributeError:
logger.exception("Unbind not successful.") # <-- full ERROR traceback
self._con = None
When bind() fails against an unreachable server, _con is a ReconnectLDAPObject whose underlying _l was never established. A later unbind() (e.g. from __del__) then calls unbind_s() → self._l.unbind_ext → ReconnectLDAPObject.__getattr__ raises:
AttributeError: ReconnectLDAPObject has no attribute '_l'
The except AttributeError catches it (good), but logger.exception(...) emits a full traceback at ERROR level for what is a perfectly expected "server was never reachable" teardown.
Reproduction
import logging, ldap
logging.basicConfig(level=logging.DEBUG)
from node.ext.ldap import LDAPProps, LDAPConnector
props = LDAPProps(uri="ldap://127.0.0.1:1", user="", password="", cache=False)
con = LDAPConnector(props=props)
try:
con.bind() # raises ldap.SERVER_DOWN (expected)
except ldap.SERVER_DOWN:
pass
con.unbind() # logs full "Unbind not successful." traceback at ERROR
Resulting log:
ERROR node.ext.ldap: Unbind not successful.
Traceback (most recent call last):
File ".../node/ext/ldap/base.py", line 161, in unbind
self._con.unbind_s()
...
File ".../ldap/ldapobject.py", line 162, in __getattr__
raise AttributeError(...)
AttributeError: ReconnectLDAPObject has no attribute '_l'
Impact
Benign (the error is caught and unbind completes), but downstream packages that are specifically designed to tolerate an unreachable LDAP server (e.g. pas.plugins.ldap, which even throttles its own LDAP-error logging) get spammed with ERROR tracebacks whenever the directory is down.
Suggested fix
Treat "connection was never established" as a non-error during unbind, e.g.:
- log at
debug instead of exception, or
- only call
unbind_s() when the connection was actually bound (guard on the missing _l / connection state), or
- narrow the message so it does not carry a full traceback.
Happy to send a PR if you agree on the preferred approach.
Summary
When the LDAP server is unreachable,
LDAPConnector.unbind()logs a full ERROR-level traceback ("Unbind not successful.") on teardown, even though the situation is handled. This pollutes logs with tracebacks on every connection failure.Found while testing
pas.plugins.ldapagainst node.ext.ldap 2.0.0 (Python 3.14, python-ldap 3.4.x).Cause
In
node/ext/ldap/base.py:When
bind()fails against an unreachable server,_conis aReconnectLDAPObjectwhose underlying_lwas never established. A laterunbind()(e.g. from__del__) then callsunbind_s()→self._l.unbind_ext→ReconnectLDAPObject.__getattr__raises:The
except AttributeErrorcatches it (good), butlogger.exception(...)emits a full traceback at ERROR level for what is a perfectly expected "server was never reachable" teardown.Reproduction
Resulting log:
Impact
Benign (the error is caught and
unbindcompletes), but downstream packages that are specifically designed to tolerate an unreachable LDAP server (e.g.pas.plugins.ldap, which even throttles its own LDAP-error logging) get spammed with ERROR tracebacks whenever the directory is down.Suggested fix
Treat "connection was never established" as a non-error during unbind, e.g.:
debuginstead ofexception, orunbind_s()when the connection was actually bound (guard on the missing_l/ connection state), orHappy to send a PR if you agree on the preferred approach.