Bug: sdk/helper/ldaputil: errwrap `{{err}}` placeholder passed to fmt.Errorf, producing malformed error messages
- Dominant language
- Go
- Stars
- 36.3k
- Forks
- 4.8k
- PR merge metrics
- PR metrics pending
Description
### Describe the bug
First recorded: https://github.com/hashicorp/vault-plugin-secrets-openldap/pull/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:
```go
retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err))
```
### To Reproduce
1. 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`.
2. 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//
```
3. 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](https://github.com/hashicorp/vault-plugin-secrets-openldap/pull/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
```go
// 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
Contributor guide
Research direction
Start in sdk/helper/ldaputil/client.go and inspect the six listed call sites, along with how their errors are consumed by the LDAP auth method and secrets engines. Verify the resulting messages omit the literal {{err}} and %!(EXTRA ...) marker, and that callers can unwrap the underlying LDAP error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend, security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100