Describe the bug
First recorded: hashicorp/vault-plugin-secrets-openldap#183
Six call sites in sdk/helper/ldaputil/client.go build a format string containing the errwrap placeholder {{err}} and pass it to fmt.Errorf along with an error argument. fmt.Errorf does not interpret {{err}}, and the format string has no verb to consume the argument, so Go appends %!(EXTRA ...).
The result is a user-facing error containing both an uninterpreted {{err}} literal and a Go formatting-error marker:
error connecting to host "ldaps://dc1.example.com:636": {{err}}%!(EXTRA *ldap.Error=LDAP Result Code 200 "Network Error": dial tcp 10.0.0.1:636: i/o timeout)
Expected:
error connecting to host "ldaps://dc1.example.com:636": LDAP Result Code 200 "Network Error": dial tcp 10.0.0.1:636: i/o timeout
This looks like leftover from the errwrap → fmt migration. Elsewhere in the codebase {{err}} is used correctly with errwrap.Wrapf, which does interpret it; these six use fmt.Errorf, which does not.
A secondary consequence: because %w is not used, the underlying error is not wrapped, so errors.Is / errors.As cannot reach it.
Affected lines
All in sdk/helper/ldaputil/client.go, present on main at c6c940dde7e050c9cc5f41cfc7329227a85115ad:
| Line |
Message |
| 40 |
error parsing url %q |
| 107 |
error connecting to host %q |
| 456 |
SID %#v convert failed reading Revision |
| 460 |
SID %#v convert failed reading SubAuthorityCount |
| 464 |
SID %#v convert failed reading IdentifierAuthority |
| 470 |
SID %#v convert failed reading SubAuthority |
For example, line 107:
retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err))
To Reproduce
- Configure any component that uses
ldaputil against an unreachable host or port — the LDAP auth method, or the LDAP/AD secrets engine via vault-plugin-secrets-openldap.
- Trigger an operation that must reach the directory. For the secrets engine, issuing or revoking a dynamic credential is enough:
vault lease revoke -sync ldap/creds/<role>/<lease-id>
- Observe the returned error.
Expected behavior
The underlying LDAP error is reported without the {{err}} literal or the %!(EXTRA ...) marker, and is wrapped such that errors.Is / errors.As work.
Impact
- User-facing error messages are malformed.
- Log-based alerting that pattern-matches on these error strings will not match, because the literal text differs from what the message is meant to say. This is the practical problem: the connection failure is still legible to a human inside the
EXTRA block, but not to a monitoring rule.
- Callers cannot unwrap to inspect the underlying
*ldap.Error.
Affects both the built-in LDAP auth method (builtin/credential/ldap) and the LDAP/AD secrets engines, which reach ldaputil through vault-plugin-secrets-openldap.
Prior mention
This was noticed and correctly attributed during unrelated work in vault-plugin-secrets-openldap#183, which contains the same output and the note:
(Note the {{err}}%!(EXTRA issue is coming from the ldaputil client).
It does not appear to have been filed separately, which is presumably why it is still present.
Suggested fix
// before
fmt.Errorf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err)
// after
fmt.Errorf("error connecting to host %q: %w", uut, err)
Applied to all six call sites. Using %w also restores errors.Is / errors.As support.
Environment
- Vault version: present on
main at c6c940dde7 (2026-07-31)
- SDK versions checked:
v0.23.0, v0.24.0, v0.25.1 — all six occurrences present in each
- Age: line 107 last modified in
cc570c11bb (2022-10-26)
- Observed on: Vault 2.0.1+ent with
vault-plugin-secrets-openldap, against Windows Server 2022 AD over LDAPS
Describe the bug
First recorded: hashicorp/vault-plugin-secrets-openldap#183
Six call sites in
sdk/helper/ldaputil/client.gobuild a format string containing theerrwrapplaceholder{{err}}and pass it tofmt.Errorfalong with anerrorargument.fmt.Errorfdoes not interpret{{err}}, and the format string has no verb to consume the argument, so Go appends%!(EXTRA ...).The result is a user-facing error containing both an uninterpreted
{{err}}literal and a Go formatting-error marker:Expected:
This looks like leftover from the
errwrap→fmtmigration. Elsewhere in the codebase{{err}}is used correctly witherrwrap.Wrapf, which does interpret it; these six usefmt.Errorf, which does not.A secondary consequence: because
%wis not used, the underlying error is not wrapped, soerrors.Is/errors.Ascannot reach it.Affected lines
All in
sdk/helper/ldaputil/client.go, present onmainatc6c940dde7e050c9cc5f41cfc7329227a85115ad:error parsing url %qerror connecting to host %qSID %#v convert failed reading RevisionSID %#v convert failed reading SubAuthorityCountSID %#v convert failed reading IdentifierAuthoritySID %#v convert failed reading SubAuthorityFor example, line 107:
To Reproduce
ldaputilagainst an unreachable host or port — the LDAP auth method, or the LDAP/AD secrets engine viavault-plugin-secrets-openldap.Expected behavior
The underlying LDAP error is reported without the
{{err}}literal or the%!(EXTRA ...)marker, and is wrapped such thaterrors.Is/errors.Aswork.Impact
EXTRAblock, but not to a monitoring rule.*ldap.Error.Affects both the built-in LDAP auth method (
builtin/credential/ldap) and the LDAP/AD secrets engines, which reachldaputilthroughvault-plugin-secrets-openldap.Prior mention
This was noticed and correctly attributed during unrelated work in vault-plugin-secrets-openldap#183, which contains the same output and the note:
It does not appear to have been filed separately, which is presumably why it is still present.
Suggested fix
Applied to all six call sites. Using
%walso restoreserrors.Is/errors.Assupport.Environment
mainatc6c940dde7(2026-07-31)v0.23.0,v0.24.0,v0.25.1— all six occurrences present in eachcc570c11bb(2022-10-26)vault-plugin-secrets-openldap, against Windows Server 2022 AD over LDAPS