From fcf50e35d077505cde7cdb1a715b622b40bf7dc7 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 7 Sep 2026 12:27:04 +0100 Subject: [PATCH] Release the connection for each redirect hop in parse(URL) SchemaTypeLoaderBase.parse(URL) follows up to five redirects. Each pass opens a connection and calls getResponseCode(), which reads the response, then moves on to the next URL without touching that connection again. Only the final one is closed, by the try-with-resources around getInputStream(). The redirect bodies are never read, so the sockets sit in the keep-alive cache holding a descriptor until a finalizer or the cache timeout reclaims them. Call disconnect() on the hop being abandoned instead. Added ParseUrlRedirectTest, a loopback HttpServer serving a three-hop redirect chain, to pin the follow behaviour - it passes either way and guards against disconnect() breaking the chain. Co-Authored-By: Claude Opus 5 (1M context) --- .../impl/schema/SchemaTypeLoaderBase.java | 3 + .../impl/schema/ParseUrlRedirectTest.java | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/test/java/org/apache/xmlbeans/impl/schema/ParseUrlRedirectTest.java diff --git a/src/main/java/org/apache/xmlbeans/impl/schema/SchemaTypeLoaderBase.java b/src/main/java/org/apache/xmlbeans/impl/schema/SchemaTypeLoaderBase.java index 09fb3cc49..1ea94e32c 100644 --- a/src/main/java/org/apache/xmlbeans/impl/schema/SchemaTypeLoaderBase.java +++ b/src/main/java/org/apache/xmlbeans/impl/schema/SchemaTypeLoaderBase.java @@ -199,6 +199,9 @@ public XmlObject parse(URL url, SchemaType type, XmlOptions options) throws XmlE } else { url = new URL(newLocation); count++; + // the redirect body is never read, so hand the socket back + // instead of leaving it for the keep-alive cache to reap + httpcon.disconnect(); } } } diff --git a/src/test/java/org/apache/xmlbeans/impl/schema/ParseUrlRedirectTest.java b/src/test/java/org/apache/xmlbeans/impl/schema/ParseUrlRedirectTest.java new file mode 100644 index 000000000..879116632 --- /dev/null +++ b/src/test/java/org/apache/xmlbeans/impl/schema/ParseUrlRedirectTest.java @@ -0,0 +1,71 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlbeans.impl.schema; + +import com.sun.net.httpserver.HttpServer; +import org.apache.xmlbeans.XmlBeans; +import org.apache.xmlbeans.XmlObject; +import org.junit.jupiter.api.Test; + +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ParseUrlRedirectTest { + + private static final String DOC = "hello"; + + @Test + void followsRedirectsToTheDocument() throws Exception { + HttpServer server = HttpServer.create( + new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); + + String base = "http://" + server.getAddress().getHostString() + ":"; + + // /0 -> /1 -> /2 -> /doc, so the loop discards several connections on the way + for (int hop = 0; hop < 3; hop++) { + String next = (hop == 2) ? "/doc" : "/" + (hop + 1); + server.createContext("/" + hop, exchange -> { + exchange.getResponseHeaders().add("Location", + base + server.getAddress().getPort() + next); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_MOVED_TEMP, -1); + exchange.close(); + }); + } + + server.createContext("/doc", exchange -> { + byte[] body = DOC.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "text/xml"); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + + server.start(); + try { + URL url = new URL(base + server.getAddress().getPort() + "/0"); + XmlObject parsed = XmlBeans.getContextTypeLoader().parse(url, null, null); + assertEquals(DOC, parsed.xmlText()); + } finally { + server.stop(0); + } + } +}