Skip to content

8343791: Socket.connect API should document whether the socket will be closed when hostname resolution fails or another error occurs #22160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 42 commits into from
Closed
Changes from 2 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8a36a88
8343791: Close `Socket` on `connect()` failures
vy Nov 12, 2024
435f15d
Apply review suggestions by Alan & Daniel
vy Nov 18, 2024
7e633d8
Apply review feedback from Alan
vy Nov 19, 2024
424fc8c
Incorporate more review feedback
vy Nov 20, 2024
270b4f9
Update docs for `connect()` not closing on `UHE`
vy Nov 20, 2024
a21c5e0
Use safe-`close()` before throwing `UHE` in `connect()`
vy Nov 20, 2024
ce56802
Catch all `bind()` and `connect()` exceptions in ctor
vy Nov 20, 2024
4ea06d8
Merge master
vy Nov 20, 2024
0761b36
Fix `connect()` Javadoc on `UHE` results in socket close
vy Nov 20, 2024
92ef9eb
Replace `UnknownHostException exception` with `var uhe`
vy Nov 20, 2024
fbcec70
Remove unused `port` variable
vy Nov 21, 2024
7d96bff
Add back incorrectly removed `SocketTimeoutException` Javadoc
vy Nov 21, 2024
385d6b2
Remove self-reference guard in `closeSuppressingExceptions()`
vy Nov 21, 2024
d3568d1
Revert `UHE` message change in `NioSocketImpl`
vy Nov 21, 2024
9f00f61
Handle `SocketTimeoutException` in `NioSocketImpl::connect()`
vy Nov 21, 2024
e37ee84
Improve naming in tests
vy Nov 22, 2024
35d6801
Improve variable names
vy Nov 25, 2024
36dd2a1
Reflect behavioral changes to `SocketAdaptor`
vy Nov 25, 2024
423a78c
Merge remote-tracking branch 'upstream/master' into Socket-CloseOnFai…
vy Nov 25, 2024
6b13675
Relax assertion to fix failing `IDNTest`
vy Nov 25, 2024
12d1947
Add tests with real sockets
vy Nov 25, 2024
afc9c63
Add tests using real sockets
vy Nov 26, 2024
e0fb706
Merge remote-tracking branch 'upstream/master' into Socket-CloseOnFai…
vy Nov 26, 2024
1e3a52c
Split tests into multiple files
vy Nov 26, 2024
aaf398f
Fix indentation in `SocketAdaptor`
vy Nov 27, 2024
dfb1bb9
Remove tests using mocks
vy Nov 27, 2024
9454364
Replace complex JUnit machinery with native Java
vy Nov 27, 2024
c8bc5a3
Enforce loopback address in the ephemeral `ServerSocket`
vy Nov 28, 2024
c1a558b
Remove forgotten `ThrowingSocketImpl`
vy Nov 28, 2024
e919edb
Use `ParameterizedTest` in `ConnectFailTest`
vy Nov 28, 2024
f7b1e09
Rework functional interfaces in tests
vy Nov 28, 2024
e02a116
Don't exceed 120 characters per line
vy Nov 28, 2024
ecbcb2b
Extend `translateToSocketException()` with `AlreadyConnectedException`
vy Nov 29, 2024
348d382
Further simplify tests
vy Nov 29, 2024
396a115
Use more fine-grained suppression
vy Nov 29, 2024
33fad08
Remove redundant `ServerSocket` background consumer process
vy Nov 29, 2024
7d56283
Merge remote-tracking branch 'upstream/master' into Socket-CloseOnFai…
vy Nov 29, 2024
840fe7a
Capitalize `already connected` error message
vy Nov 29, 2024
b6a2c8c
Merge remote-tracking branch 'upstream/master' into Socket-CloseOnFai…
vy Nov 29, 2024
e5d1cb2
Match `UHE` message in `Socket` and `SocketImpl`
vy Nov 29, 2024
d69d14c
Improve Javadoc
vy Nov 29, 2024
f39479d
Reword the `ConnectFailTest` summary
vy Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 130 additions & 131 deletions test/jdk/java/net/Socket/ConnectFailTest.java
Original file line number Diff line number Diff line change
@@ -23,14 +23,23 @@

import jdk.test.lib.Utils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -55,63 +64,28 @@ class ConnectFailTest {
InetSocketAddress.createUnresolved("no.such.host", DEAD_SERVER_PORT);

@Test
void verifyUnresolvedAddress() {
void testUnresolvedAddress() {
assertTrue(UNRESOLVED_ADDRESS.isUnresolved());
}

// Socket should be closed when `unboundSocket.connect()` fails ////////////////////////////////////////////////////

@Test
void unboundSocketShouldBeClosedWhenConnectFails() throws Exception {
unboundSocketShouldBeClosedWhenConnectFails(ConnectFailTest::createUnboundSocket);
}

@Test
void unboundNioSocketShouldBeClosedWhenConnectFails() throws Exception {
unboundSocketShouldBeClosedWhenConnectFails(ConnectFailTest::createUnboundNioSocket);
}

private static void unboundSocketShouldBeClosedWhenConnectFails(
ThrowingSupplier<Socket> unboundSocketFactory)
throws Exception {
try (Socket socket = unboundSocketFactory.get()) {
assertFalse(socket.isBound());
assertFalse(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(REFUSING_SOCKET_ADDRESS));
assertTrue(socket.isClosed());
}
}

// Socket should be closed when `boundSocket.connect()` fails //////////////////////////////////////////////////////

@Test
void boundSocketShouldBeClosedWhenConnectFails() throws Exception {
boundSocketShouldBeClosedWhenConnectFails(
ConnectFailTest::createBoundSocket);
}

@Test
void boundNioSocketShouldBeClosedWhenConnectFails() throws Exception {
boundSocketShouldBeClosedWhenConnectFails(
ConnectFailTest::createBoundNioSocket);
}

private static void boundSocketShouldBeClosedWhenConnectFails(
ThrowingSupplier<Socket> boundSocketFactory)
throws Exception {
try (Socket socket = boundSocketFactory.get()) {
/**
* Verifies socket is closed when {@code unboundSocket.connect()} fails.
* @param socket an unbound socket
*/
@ParameterizedTest
@MethodSource("boundSockets")
void testBoundSocket(Socket socket) throws IOException {
try (socket) {
assertTrue(socket.isBound());
assertFalse(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(REFUSING_SOCKET_ADDRESS));
assertTrue(socket.isClosed());
}
}

// Socket should *NOT* be closed when `connectedSocket.connect()` fails ////////////////////////////////////////////

@Test
void connectedSocketShouldNotBeClosedWhenConnectFails() throws Exception {
connectedSocketShouldNotBeClosedWhenConnectFails(
void testConnectedSocket() throws Exception {
testConnectedSocket(
ConnectFailTest::createConnectedSocket,
runnable -> {
SocketException exception = assertThrows(SocketException.class, runnable::run);
@@ -120,17 +94,22 @@ void connectedSocketShouldNotBeClosedWhenConnectFails() throws Exception {
}

@Test
void connectedNioSocketShouldNotBeClosedWhenConnectFails() throws Exception {
connectedSocketShouldNotBeClosedWhenConnectFails(
void testConnectedNioSocket() throws Exception {
testConnectedSocket(
ConnectFailTest::createConnectedNioSocket,
runnable -> assertThrows(AlreadyConnectedException.class, runnable::run));
}

private static void connectedSocketShouldNotBeClosedWhenConnectFails(
/**
* Verifies socket is not closed when {@code `connectedSocket.connect()`} fails.
* @param connectedSocketFactory a connected socket factory
* @param reconnectFailureVerifier a consumer verifying the thrown reconnect failure
*/
private static void testConnectedSocket(
ThrowingFunction<SocketAddress, Socket> connectedSocketFactory,
Consumer<ThrowingRunnable> reconnectFailureVerifier)
throws Exception {
ServerSocketTestUtil.withEphemeralServerSocket(serverSocket -> {
withEphemeralServerSocket(serverSocket -> {
SocketAddress serverSocketAddress = serverSocket.getLocalSocketAddress();
try (Socket socket = connectedSocketFactory.apply(serverSocketAddress)) {
assertTrue(socket.isBound());
@@ -143,127 +122,147 @@ private static void connectedSocketShouldNotBeClosedWhenConnectFails(
});
}

// Socket should be closed when `unboundSocket.connect(unresolvedAddress)` fails ///////////////////////////////////

@Test
void unboundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
unboundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createUnboundSocket);
}

private static Socket createUnboundSocket() {
return new Socket();
}

@Test
void unboundNioSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
unboundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createUnboundNioSocket);
}

@SuppressWarnings("resource")
private static Socket createUnboundNioSocket() throws IOException {
return SocketChannel.open().socket();
}

private static void unboundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ThrowingSupplier<Socket> unboundSocketFactory)
throws Exception {
try (Socket socket = unboundSocketFactory.get()) {
/**
* Verifies socket is closed when {@code unboundSocket.connect(unresolvedAddress)} fails.
* @param socket an unbound socket
*/
@ParameterizedTest
@MethodSource("unboundSockets")
void testUnboundSocketWithUnresolvedAddress(Socket socket) throws IOException {
try (socket) {
assertFalse(socket.isBound());
assertFalse(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(UNRESOLVED_ADDRESS));
assertTrue(socket.isClosed());
}
}

// Socket should be closed when `boundSocket.connect(unresolvedAddress)` fails /////////////////////////////////////

@Test
void boundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
boundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createBoundSocket);
}

private static Socket createBoundSocket() throws IOException {
Socket socket = new Socket();
socket.bind(new InetSocketAddress(0));
return socket;
}

@Test
void boundNioSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
boundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createBoundNioSocket);
}

@SuppressWarnings("resource")
private static Socket createBoundNioSocket() throws IOException {
SocketChannel channel = SocketChannel.open();
channel.bind(new InetSocketAddress(0));
return channel.socket();
static List<Socket> unboundSockets() throws IOException {
return List.of(
new Socket(),
SocketChannel.open().socket());
}

private static void boundSocketShouldBeClosedWhenConnectWithUnresolvedAddressFails(
ThrowingSupplier<Socket> boundSocketFactory)
throws Exception {
try (Socket socket = boundSocketFactory.get()) {
/**
* Verifies socket is closed when {@code boundSocket.connect(unresolvedAddress)} fails.
* @param socket a bound socket
*/
@ParameterizedTest
@MethodSource("boundSockets")
void testBoundSocketWithUnresolvedAddress(Socket socket) throws IOException {
try (socket) {
assertTrue(socket.isBound());
assertFalse(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(UNRESOLVED_ADDRESS));
assertTrue(socket.isClosed());
}
}

// Socket should *NOT* be closed when `connectedSocket.connect(unresolvedAddress)` fails ///////////////////////////
@SuppressWarnings("resource")
static List<Socket> boundSockets() throws IOException {
List<Socket> sockets = new ArrayList<>();
// Socket
{
Socket socket = new Socket();
socket.bind(new InetSocketAddress(0));
sockets.add(socket);
}
// NIO Socket
{
SocketChannel channel = SocketChannel.open();
channel.bind(new InetSocketAddress(0));
sockets.add(channel.socket());
}
return sockets;
}

@Test
void connectedSocketShouldNotBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
connectedSocketShouldNotBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createConnectedSocket);
/**
* Verifies socket is not closed when {@code connectedSocket.connect(unresolvedAddress)} fails.
* @param connectedSocketFactory a connected socket factory
*/
@ParameterizedTest
@MethodSource("connectedSocketFactories")
void testConnectedSocketWithUnresolvedAddress(
ThrowingFunction<SocketAddress, Socket> connectedSocketFactory)
throws Exception {
withEphemeralServerSocket(serverSocket -> {
SocketAddress serverSocketAddress = serverSocket.getLocalSocketAddress();
try (Socket socket = connectedSocketFactory.apply(serverSocketAddress)) {
assertTrue(socket.isBound());
assertTrue(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(UNRESOLVED_ADDRESS));
assertFalse(socket.isClosed());
}
});
}

static List<ThrowingFunction<SocketAddress, Socket>> connectedSocketFactories() {
return List.of(
ConnectFailTest::createConnectedSocket,
ConnectFailTest::createConnectedNioSocket);
}

private static Socket createConnectedSocket(SocketAddress address) throws IOException {
InetSocketAddress inetAddress = (InetSocketAddress) address;
return new Socket(inetAddress.getAddress(), inetAddress.getPort());
}

@Test
void connectedNioSocketShouldNotBeClosedWhenConnectWithUnresolvedAddressFails() throws Exception {
connectedSocketShouldNotBeClosedWhenConnectWithUnresolvedAddressFails(
ConnectFailTest::createConnectedNioSocket);
}

@SuppressWarnings("resource")
private static Socket createConnectedNioSocket(SocketAddress address) throws IOException {
return SocketChannel.open(address).socket();
}

private static void connectedSocketShouldNotBeClosedWhenConnectWithUnresolvedAddressFails(
ThrowingFunction<SocketAddress, Socket> connectedSocketFactory)
throws Exception {
ServerSocketTestUtil.withEphemeralServerSocket(serverSocket -> {
SocketAddress serverSocketAddress = serverSocket.getLocalSocketAddress();
try (Socket socket = connectedSocketFactory.apply(serverSocketAddress)) {
assertTrue(socket.isBound());
assertTrue(socket.isConnected());
assertThrows(IOException.class, () -> socket.connect(UNRESOLVED_ADDRESS));
assertFalse(socket.isClosed());
private static void withEphemeralServerSocket(ThrowingConsumer<ServerSocket> serverSocketConsumer) throws Exception {
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
ServerSocket serverSocket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress())) {
// Accept connections in the background to avoid blocking the caller
executorService.submit(() -> acceptConnections(serverSocket));
serverSocketConsumer.accept(serverSocket);
}
}

@SuppressWarnings("ResultOfMethodCallIgnored")
private static void acceptConnections(ServerSocket serverSocket) {
System.err.println("[Test socket server] Starting accepting connections");
while (true) {
try {

// Accept the connection
Socket clientSocket = serverSocket.accept();
System.err.format(
"[Test socket server] Accepted port %d to port %d%n",
((InetSocketAddress) clientSocket.getRemoteSocketAddress()).getPort(),
clientSocket.getLocalPort());

// Instead of directly closing the socket, we try to read some to block. Directly closing
// the socket will invalidate the client socket tests checking the established connection
// status.
try (clientSocket; InputStream inputStream = clientSocket.getInputStream()) {
inputStream.read();
} catch (IOException _) {
// Do nothing
}

} catch (IOException _) {
break;
}
});

}
System.err.println("[Test socket server] Stopping accepting connections");
}

@FunctionalInterface
private interface ThrowingRunnable {
private interface ThrowingConsumer<V> {

void run() throws Exception;
void accept(V value) throws Exception;

}

@FunctionalInterface
private interface ThrowingSupplier<V> {
private interface ThrowingRunnable {

V get() throws Exception;
void run() throws Exception;

}

Loading