Socket and Stream Errors can be Annoyingly Non-Specific
Let us say you are using a Java library that is making connections to some selection of endpoints. Perhaps a web spider, or a data layer communicating to multiple client nodes in an Elasticsearch cluster, or something of this nature. There is more than one endpoint for socket connections, the library is choosing which endpoint to use at any given point in time, data is passing back and forth over streams, and the low level operations with Socket, InputStream, and OutputStream instances are buried deep in the library, as they should be.
What happens when an IOException occurs, such as for a timeout or failure to connect? The odds are fairly good that the library will just pass that exception up the stack verbatim, and as a result it won't actually identify the specific endpoint affected. Which is terrible, and if you happen to be writing a library of this nature, please don't do this. Nonetheless, a sizable fraction of the time you will find that you have the exception in your logs, telling you that a connection or data transfer failed in some way, with a stack trace leading all the way down into the depths, but the most useful information, meaning which endpoint was the problem, is not attached to it.
If you are lucky, then by looking through the library code, you may find that this information is logged somewhere useful at debug level - i.e. the library intercepts an IOException from a socket or stream instance, logs something based on the context in which that instance is used, and then rethrows. Why do this rather than wrap the exception such that it contains the additional information? Who knows. Library authors are inscrutable. If this is the case, it is then a matter of hoping that the log line is in a place that will not bury the log files in gigabytes of entries if debug level logging is enabled for the relevant class. Sadly this is all too rarely the case. People like their debug level logging to be voluble, it seems.
Another option is to somehow inject your own wrappers or subclasses of Socket, InputStream, and OutputStream that do in fact provide the desired information when they throw. For example, given a library that makes HTTP/S connections, if it allows configuration via an Apache library HttpClientConfig instance, something like the following can work:
// Set up factories that use an overridden socket that throws more // informative exceptions. PlainConnectionSocketFactory plainSocketFactory = new PlainConnectionSocketFactory() { @Override public Socket createSocket(final HttpContext context) throws IOException { return new InformativeSocket(); } }; SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( SSLContexts.createDefault(), SSLConnectionSocketFactory.getDefaultHostnameVerifier() ) { @Override public Socket createSocket(final HttpContext context) throws IOException { // The parent class makes a more complicated call that passes // through numerous layers to eventually just create a plain // Socket instance. return new InformativeSocket(); } }; HttpClientConfig httpClientConfig = new HttpClientConfig.Builder(Collections.emptyList()) .connTimeout(CONNECTION_TIMEOUT_MS) .readTimeout(READ_TIMEOUT_MS) .maxTotalConnection(MAX_CONNECTIONS) .defaultMaxTotalConnectionPerRoute(MAX_CONNECTIONS) // ... and so on for other options // // Specify the socket factories that allow for injection of more useful // Socket implementations. .plainSocketFactory(plainSocketFactory) .sslSocketFactory(sslSocketFactory) // And obtain the config we will use. .build();
What would the InformativeSocket class look like? The following is one example, wrapping the applicable methods in order to add information to the exceptions thrown:
package com.example.informative; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.SocketAddress; /** * A socket subclass that adds more information to thrown exceptions. */ public class InformativeSocket extends Socket { @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { try { super.connect(endpoint, timeout); } catch (IOException e) { throw new IOException("connect() to " + endpoint + ": ", e); } } @Override public void bind(SocketAddress bindpoint) throws IOException { try { super.bind(bindpoint); } catch (IOException e) { throw new IOException("bind() to " + bindpoint + ": ", e); } } @Override public InputStream getInputStream() throws IOException { try { return new InformativeInputStreamWrapper( super.getInputStream(), getInetAddress(), getPort() ); } catch (IOException e) { throw new IOException("getInputStream() for: " + getEndpoint() + ": ", e); } } @Override public OutputStream getOutputStream() throws IOException { try { return new InformativeOutputStreamWrapper( super.getOutputStream(), getInetAddress(), getPort() ); } catch (IOException e) { throw new IOException("getOutputStream() for: " + getEndpoint() + ": ", e); } } @Override public void sendUrgentData (int data) throws IOException { try { super.sendUrgentData(data); } catch (IOException e) { throw new IOException("sendUrgentData() for: " + getEndpoint() + ": ", e); } } @Override public void shutdownInput() throws IOException { try { super.shutdownInput(); } catch (IOException e) { throw new IOException("shutdownInput() for: " + getEndpoint() + ": ", e); } } @Override public void shutdownOutput() throws IOException { try { super.shutdownOutput(); } catch (IOException e) { throw new IOException("shutdownOutput() for: " + getEndpoint() + ": ", e); } } private String getEndpoint() { InetAddress inetAddress = getInetAddress(); return inetAddress != null ? inetAddress.toString() + ":" + getPort(): "[not connected]"; } }
Because most of the applicable exceptions occur in the stream classes, such as timeouts for an open connection, this also requires an InformativeInputStream and InformativeOutputStream. In that case, the safest and most generally applicable approach seems to be a wrapper rather than a subclass. Examples follow:
package com.example.informative; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; /** * A wrapper for a socket-related input stream to add more useful information to * exceptions, such as the endpoint it is connected to. */ public class InformativeInputStreamWrapper extends InputStream { private final InputStream in; private final InetAddress inetAddress; private final int port; /** * Constructor providing information about the endpoint of the socket * stream. * * @param inputStream The stream to wrap. * @param inetAddress The address of the endpoint. * @param port The port of the endpoint. */ public InformativeInputStreamWrapper( InputStream inputStream, InetAddress inetAddress, int port ) { this.in = inputStream; this.inetAddress = inetAddress; this.port = port; } public int read() throws IOException { try { return in.read(); } catch (IOException e) { throw new IOException("read() from " + getEndpoint() + ": ", e); } } public int read(byte b[]) throws IOException { try { return in.read(b); } catch (IOException e) { throw new IOException("read() from " + getEndpoint() + ": ", e); } } public int read(byte b[], int off, int len) throws IOException { try { return in.read(b, off, len); } catch (IOException e) { throw new IOException("read() from " + getEndpoint() + ": ", e); } } public long skip(long n) throws IOException { try { return in.skip(n); } catch (IOException e) { throw new IOException("skips() from " + getEndpoint() + ": ", e); } } public int available() throws IOException { try { return in.available(); } catch (IOException e) { throw new IOException("available() from " + getEndpoint() + ": ", e); } } public void close() throws IOException { try { in.close(); } catch (IOException e) { throw new IOException("close() from " + getEndpoint() + ": ", e); } } public synchronized void mark(int readlimit) { in.mark(readlimit); } public synchronized void reset() throws IOException { try { in.reset(); } catch (IOException e) { throw new IOException("reset() from " + getEndpoint() + ": ", e); } } public boolean markSupported() { return in.markSupported(); } private String getEndpoint() { return inetAddress != null ? inetAddress.toString() + ":" + port: "[not connected]"; } }
package com.example.informative; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; /** * A wrapper for a socket-related output stream to add more useful information to * exceptions, such as the endpoint it is connected to. */ public class InformativeOutputStreamWrapper extends OutputStream { private final OutputStream out; private final InetAddress inetAddress; private final int port; /** * Constructor providing information about the endpoint of the socket * stream. * * @param inputStream The stream to wrap. * @param inetAddress The address of the endpoint. * @param port The port of the endpoint. */ public InformativeOutputStreamWrapper(OutputStream inputStream, InetAddress inetAddress, int port) { this.out = inputStream; this.inetAddress = inetAddress; this.port = port; } public void write(int b) throws IOException { try { out.write(b); } catch (IOException e) { throw new IOException("write() to " + getEndpoint() + ": ", e); } } public void write(byte b[], int off, int len) throws IOException { try { out.write(b, off, len); } catch (IOException e) { throw new IOException("write() to " + getEndpoint() + ": ", e); } } public void flush() throws IOException { try { out.flush(); } catch (IOException e) { throw new IOException("flush() to " + getEndpoint() + ": ", e); } } public void close() throws IOException { try { out.close(); } catch (IOException e) { throw new IOException("close() to " + getEndpoint() + ": ", e); } } private String getEndpoint() { return inetAddress != null ? inetAddress.toString() + ":" + port: "[not connected]"; } }
The exercise has become fairly elaborate at this point, given that all we are after is the one tiny piece of information about the context of an exception. Certainly, if there are other easier ways to obtain the connection endpoint associated with an exception, that would be preferable. Sadly, sometimes those easier paths are not available without, say, going through the formal process of submitting a patch or pull request to a library, when all you want is the endpoint information right now, today.