8000 Async ResolverListener receives wrapped CompletionException on timeouts, inconsistent with sync send() · Issue #383 · dnsjava/dnsjava · GitHub
[go: up one dir, main page]

Skip to content
Async ResolverListener receives wrapped CompletionException on timeouts, inconsistent with sync send() #383
Closed
@bhaveshthakker

Description

@bhaveshthakker

Summary
In dnsjava 3.x, when using the asynchronous ExtendedResolver.sendAsync() with a ResolverListener, timeout exceptions (IOException) are wrapped in a java.util.concurrent.CompletionException.

This behavior represents two issues for users:

  1. A Breaking Change(backward incompatibility): It is a departure from dnsjava 1.x, where the listener would receive the raw IOException. This breaks compatibility for applications migrating from the previous version.

  2. An Internal Inconsistency: It is inconsistent with the synchronous Resolver.send() method within dnsjava 3.x, which correctly unwraps the CompletionException and re-throws the original IOException.

This forces users of the ResolverListener pattern to add boilerplate code to manually unwrap the exception.

Steps to Reproduce
Create an ExtendedResolver pointing to a non-responsive IP address to guarantee a timeout.

Use the sendAsync(Message, ResolverListener) method.

In the ResolverListener.handleException() implementation, print the type of the received exception.

Conceptual Code Example:

Java

import org.xbill.DNS.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class AsyncTimeoutIssue {
  public static void main(String[] args) throws Exception {
    // 1. Point to a blackhole address to ensure a timeout
    ExtendedResolver resolver = new ExtendedResolver();
    resolver.addServer(new InetSocketAddress("192.0.2.1", 53)); // RFC 5737 TEST-NET-1
    resolver.setTimeout(2); // 2 seconds

    Message query = Message.newQuery(Record.newRecord(Name.fromString("example.com."), Type.A, DClass.IN));
    CountDownLatch latch = new CountDownLatch(1);

    // 2. Use the async method with a listener
    resolver.sendAsync(
        query,
        new ResolverListener() {
          @Override
          public void receiveMessage(Object id, Message m) {
            System.out.println("Received message (should not happen)");
            latch.countDown();
          }

          @Override
          public void handleException(Object id, Exception ex) {
            // 3. Observe the exception type
            System.out.println("Exception received in listener: " + ex.getClass().getName());
            if (ex instanceof CompletionException) {
              System.out.println("Cause of CompletionException: " + ex.getCause().getClass().getName());
            }
            latch.countDown();
          }
        });

    latch.await(5, TimeUnit.SECONDS);
  }
}

Expected Behavior
To maintain backward compatibility with dnsjava 1.x and for API consistency within dnsjava 3.x itself, the ResolverListener.handleException() method should receive the original IOException that caused the failure, not the CompletionException wrapper.

The output of the example should show that the exception is java.io.IOException (or a subclass like org.xbill.DNS.exception.TimeoutException).

// Expected Output
Exception received in listener: org.xbill.DNS.exception.TimeoutException
Actual Behavior
The handleException method receives a java.util.concurrent.CompletionException, which wraps the underlying IOException.

// Actual Output
Exception received in listener: java.util.concurrent.CompletionException
Cause of CompletionException: org.xbill.DNS.exception.TimeoutException
Analysis and Proposed Solution
This behavior stems from the use of CompletionStage internally. While the synchronous CompletableFuture.get() path correctly unwraps the CompletionException into an ExecutionException (whose cause is then re-thrown), the async path that invokes the ResolverListener does not perform this unwrapping.

To improve API consistency, the logic that calls the ResolverListener should be updated to unwrap the CompletionException before invoking handleException(). This would align the async listener's behavior with the sync send() method and restore the expected behavior for legacy users.

A simple check and unwrap would look like this:

Java

// In the dnsjava internals that call the listener
void forwardExceptionToListener(Exception ex, ResolverListener listener, Object id) {
  Throwable cause = ex;
  if (ex instanceof CompletionException && ex.getCause() != null) {
    cause = ex.getCause();
  }

  if (cause instanceof Exception) {
    listener.handleException(id, (Exception) cause);
  } else {
    listener.handleException(id, new Exception("Wrapped non-exception throwable", cause));
  }
}

Thank you for maintaining this essential library.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

    0