You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/11 08:19:52 UTC
(grails-core) branch 8.1.x updated: Keep StartupProgressSpec off ports held on the loopback address
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch 8.1.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to refs/heads/8.1.x by this push:
new 73c234f26e Keep StartupProgressSpec off ports held on the loopback address
73c234f26e is described below
commit 73c234f26e7ac9bebb1b9e669cca0fe9939b097b
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Sun Oct 11 09:13:40 2026 +0100
Keep StartupProgressSpec off ports held on the loopback address
freePort() took whatever port a wildcard ServerSocket got. Java sets
SO_REUSEADDR on that socket, so the bind succeeds even when another
process listens on the port on 127.0.0.1 alone. The application then
binds every address as well, but the spec's requests to localhost reach
the more specific loopback listener, so whichever feature drew that port
failed with a null response. Locally the port belonged to a Go HTTPS
proxy, which answered "Client sent an HTTP request to an HTTPS server."
freePort() now also requires the port to bind on the loopback address
without address reuse. With another process holding every other
loopback port in the ephemeral range, 14 of the 27 features failed
before this change; afterwards only the random-port feature can still
draw such a port, because Tomcat picks that one itself.
pageLoad() no longer turns an IOException into a null response. Every
caller expects an answer, and the swallowed exception left the Windows
failures of the tasks feature on 9.0.x with only a NullPointerException.
---
.../grails/startup/StartupProgressSpec.groovy | 31 ++++++++++++++++++----
1 file changed, 26 insertions(+), 5 deletions(-)
diff --git a/grails-startup-progress/src/test/groovy/org/apache/grails/startup/StartupProgressSpec.groovy b/grails-startup-progress/src/test/groovy/org/apache/grails/startup/StartupProgressSpec.groovy
index 7cff20e229..7de8437175 100644
--- a/grails-startup-progress/src/test/groovy/org/apache/grails/startup/StartupProgressSpec.groovy
+++ b/grails-startup-progress/src/test/groovy/org/apache/grails/startup/StartupProgressSpec.groovy
@@ -1044,6 +1044,7 @@ class StartupProgressSpec extends Specification {
/**
* A page load as a browser makes it, with the fetch metadata browsers send on navigation. It goes
* through {@link HttpClient} because {@link HttpURLConnection} silently drops {@code Sec-} headers.
+ * Every page load here expects an answer, so one that gets none fails with the reason.
*/
private Response pageLoad(String path) {
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create("http://localhost:${port}${path}"))
@@ -1065,9 +1066,6 @@ class StartupProgressSpec extends Specification {
retryAfter: response.headers().firstValue('Retry-After').orElse(null),
body: response.body())
}
- catch (IOException ignored) {
- return null
- }
}
private static Response request(int port, String path, String accept, String method = 'GET', String cookie = null, Map<String, String> headers = [:]) {
@@ -1117,9 +1115,32 @@ class StartupProgressSpec extends Specification {
}
}
+ /**
+ * A port nothing listens on. The application binds every address, which succeeds even when another process
+ * listens on the port on the loopback address alone, and that process then answers this spec's requests to
+ * localhost, so a port held there is not free.
+ */
private static int freePort() {
- try (ServerSocket socket = new ServerSocket(0)) {
- return socket.localPort
+ for (int attempt = 0; attempt < 100; attempt++) {
+ int candidate
+ try (ServerSocket socket = new ServerSocket(0)) {
+ candidate = socket.localPort
+ }
+ if (isFreeOnLoopback(candidate)) {
+ return candidate
+ }
+ }
+ throw new IllegalStateException('No port is free on the loopback address as well as on every address')
+ }
+
+ private static boolean isFreeOnLoopback(int port) {
+ try (ServerSocket socket = new ServerSocket()) {
+ socket.reuseAddress = false
+ socket.bind(new InetSocketAddress(InetAddress.loopbackAddress, port))
+ return true
+ }
+ catch (IOException ignored) {
+ return false
}
}