From a40723b61b960fbf9a1826b32299f91f4abef306 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 5 Aug 2026 10:45:52 +0000 Subject: [PATCH] Use paged (RFC 2696) LDAP search for the directory-wide import browse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain ldapsearch against this AD with the identical broad filter (objectClass=person) completed cleanly — 996 entries, exit 0 — ruling out the network/server as the cause of the " closed" ConnectionError our app was hitting. The difference is response size: ~1000 LDAP protocol messages in one unbounded response is apparently enough to trip up ldapjs's client-side handling, even though our own busy_timeout increase (previous commit) didn't help since the connection was being closed, not timed out. Paging the search (200 entries per round-trip, handled internally by ldapjs — callers still just see one continuous stream of searchEntry events) sidesteps the issue regardless of its exact root cause. Applies to both the admin "import accounts" browse and the per-login user lookup, since they share the same search() helper. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY --- src/lib/ldap/client.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/ldap/client.ts b/src/lib/ldap/client.ts index c47b362..d85c23e 100644 --- a/src/lib/ldap/client.ts +++ b/src/lib/ldap/client.ts @@ -45,7 +45,15 @@ function search( ): Promise<{ dn: string; attributes: Record }[]> { return new Promise((resolve, reject) => { const results: { dn: string; attributes: Record }[] = []; - client.search(baseDn, { filter, scope: "sub" }, (err, res) => { + // Paged (RFC 2696) rather than one unbounded response: a directory-wide + // browse on a real AD domain can return ~1000 entries, and ldapjs's + // single-response handling for a batch that size was observed to drop + // the connection (" closed") even though a plain `ldapsearch` with + // the identical filter completes cleanly — so the fix is on our client's + // response handling, not the query itself. ldapjs runs the multi-page + // RFC 2696 round-trips internally; callers still just see one continuous + // stream of `searchEntry` events followed by a single `end`. + client.search(baseDn, { filter, scope: "sub", paged: { pageSize: 200 } }, (err, res) => { if (err) { reject(err); return;