[#1216] Keep the decoding error of the client filter when no LDAP connection is attached (#1217)
Fixes #1216
## Problem
`LDAPClientFilter.handleReadException()` and `exceptionOccurred()` close
the LDAP connection attached to the Grizzly connection without checking
that one is attached. After `handleClose()` has removed it, a message
the client cannot decode throws a `NullPointerException` in place of the
`DecodeException`. Grizzly logs the `NullPointerException` at `WARNING`
(`GRIZZLY0013`), and the decoding error with its message is lost.
The failed job of #1213 ([run
38947864951](https://github.com/OpenIdentityPlatform/OpenDJ/actions/runs/38947864951/job/117131465938))
logged exactly that: the client read its own Unbind on a connection it
had already closed.
## Change
Both methods return when no LDAP connection is attached, as
`handleClose()` and the result handlers of `ClientResponseHandler`
already do.
- `handleRead()` then throws the `DecodeException`, as it does with a
connection attached. `DefaultFilterChain.execute()` logs it at `FINE`
and closes the connection.
- In `exceptionOccurred()` the Grizzly connection is still open (the
method returns earlier otherwise), so only a connection not registered
yet has no LDAP connection. No failure was seen there. The check keeps
the method from failing the same way: an exception thrown from it inside
`DefaultFilterChain.execute()` would escape `throwChain()` before the
`closeWithReason()` that follows.
One correction to the issue: reads cannot reach `handleRead()` before
`registerConnection()`. Grizzly 4.0.2 enables reading only after the
connect completion handler has run:
`TCPNIOConnectorHandler.EnableReadHandler.onComplete()` calls
`completed()` before `enableInitialOpRead()`. The window seen in
practice is the one after `handleClose()`.
## Tests
`LDAPClientFilterTestCase` (new):
| Test | What it pins |
|---|---|
| `handleReadWithoutLDAPConnectionThrowsTheDecodeError` | No LDAP
connection attached: `handleRead()` throws `Unexpected LDAP request:
id=1, …`. Red on master with the NPE. |
| `exceptionOccurredWithoutLDAPConnectionReturns` | No LDAP connection
attached: `exceptionOccurred()` returns. Red on master with the NPE. |
| `handleReadClosesTheLDAPConnectionOnDecodeError` | With one attached,
it is still closed with `CLIENT_SIDE_DECODING_ERROR`. |
| `exceptionOccurredClosesTheLDAPConnection` | With one attached, it is
still closed with `CLIENT_SIDE_LOCAL_ERROR`. |
`GrizzlyLDAPConnection` is final, so the last two use a real one and
check the error through a `ConnectionEventListener` mock.
Mutants of `LDAPClientFilter`, each run against the class:
| Mutant | Caught by |
|---|---|
| no null check in `handleReadException()` |
`handleReadWithoutLDAPConnectionThrowsTheDecodeError` |
| no null check in `exceptionOccurred()` |
`exceptionOccurredWithoutLDAPConnectionReturns` |
| `handleReadException()` never closes |
`handleReadClosesTheLDAPConnectionOnDecodeError` |
| `exceptionOccurred()` never closes |
`exceptionOccurredClosesTheLDAPConnection` |
## Verification
- `org.forgerock.opendj.grizzly.**` in the reactor (`-pl opendj-grizzly
-am`), JDK 27, macOS: 1044 tests, 0 failures, 0 errors, 0 skipped.