Skip to content

Fix Persistent Connections (Issue 7230)#1121

Draft
Bombe wants to merge 10 commits intohyphanet:nextfrom
Bombe:fix/7230-persistent-connections
Draft

Fix Persistent Connections (Issue 7230)#1121
Bombe wants to merge 10 commits intohyphanet:nextfrom
Bombe:fix/7230-persistent-connections

Conversation

@Bombe
Copy link
Contributor

@Bombe Bombe commented Feb 7, 2026

(I inadvertently closed the old PR by removing the branch in my repository, so here is another one!)

Bug Report: https://freenet.mantishub.io/view.php?id=7230

This branch fixes the problem outlined in the bug report mentioned above, but it also does so much more. 😁 There are four types of commits:

🧑‍💻 – lays groundwork for upcoming tests. This comprises a parser for HTTP responses and Hamcrest matchers for said parsed responses, allowing more expressive tests.
⬆️ – updates dependencies. This updates to the last version of Mockito that we can use (due to its inability to mock anything involving WeakReference starting in version 3), allowing mocks of final classes and classes with final methods; required for mocking Toadlet.findSupportedMethods()!
🐛 – fixes problems I found in the ToadletContextImpl. One of them is the one from the bug report, the other ones are small issues that I found during tests.
✅ – adds additional tests.

The final barrage of tests achieves almost full coverage of the outer-most try block in ToadletContextImpl.handle().

@bertm
Copy link
Contributor

bertm commented Feb 8, 2026

TL;DR: with mockito-inline we can't mock classes when compiling on JDK 13 or later (I'm using 24).

> Task :test

1239 tests completed, 49 failed

All of the above fail due to a org.mockito.exceptions.base.MockitoException:

org.mockito.exceptions.base.MockitoException:  Mockito cannot mock this class: <class>.
Underlying exception: org.mockito.exceptions.base.MockitoException: Could not modify all classes [class java.lang.Object, <other classes>]
Caused by: org.mockito.exceptions.base.MockitoException: Could not modify all classes [class java.lang.Object, <other classes>]
Caused by: java.lang.IllegalStateException: Byte Buddy could not instrument all classes within the mock's type hierarchy
Caused by: java.lang.IllegalArgumentException: Unsupported class file major version 68

The inlining ByteBuddy mock-maker (from mockito-inline) attempts to read classes for the entire type hierarchy up to and including Object. ByteBuddy 1.9.10 only supports class file major version 56 and lower (Java 12 and earlier), so the mocks fail to create. Switching from JDK 24 to JDK 11 "solves" the problem.

Knowing this, we can't use mockito-inline as it would present a continuous effort to keep our tests compatible with reasonably recent JDKs.

@Bombe
Copy link
Contributor Author

Bombe commented Feb 8, 2026

Arne had the same issue in #1106 but didn’t mention whether it persisted for him; and as with #1106, this branch (which is basically unchanged from #1106) builds for me with basically all Java versions.

Okay, scratch that, I can reproduce the issue.

@bertm
Copy link
Contributor

bertm commented Feb 8, 2026

I can consistently reproduce this by swithing JVM through SDKMAN and running ./gradlew test.

  • OpenJDK 24+36-3646 - fails
  • OpenJDK Temurin-17.0.5+8 - fails
  • OpenJDK Temurin-11.0.25+9 - succeeds
  • OpenJDK 1.8.0_302-b08 - succeeds

@bertm
Copy link
Contributor

bertm commented Feb 8, 2026

The tests would only need minor adjustment to do without inline mocks. This is what fixes it for me:

diff --git a/build.gradle b/build.gradle
index b6864210e9..c819e937ea 100644
--- a/build.gradle
+++ b/build.gradle
@@ -264,7 +264,7 @@ dependencies {
     implementation "org.slf4j:slf4j-api:1.7.25"
 
     testImplementation 'junit:junit:4.13.2'
-    testImplementation "org.mockito:mockito-inline:2.28.2"
+    testImplementation "org.mockito:mockito-core:2.28.2"
     testImplementation "org.hamcrest:hamcrest:3.0"
 }
 
diff --git a/test/freenet/clients/http/ToadletContextImplTest.java b/test/freenet/clients/http/ToadletContextImplTest.java
index 4a4f1ee5d8..6c074c0f13 100644
--- a/test/freenet/clients/http/ToadletContextImplTest.java
+++ b/test/freenet/clients/http/ToadletContextImplTest.java
@@ -21,11 +21,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Matchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -61,9 +60,7 @@ public class ToadletContextImplTest {
 	@Test
 	public void redirectExceptionFromToadletResultsInRedirect() throws Exception {
 		setupInputStream("GET /redirect-toadlet HTTP/1.0\r\n\r\n");
-		Toadlet redirectingToadlet = mock(Toadlet.class, RETURNS_DEEP_STUBS);
-		when(redirectingToadlet.findSupportedMethods()).thenReturn("GET");
-		doThrow(new RedirectException("/new-location")).when(redirectingToadlet).handleMethodGET(any(), any(), any());
+		Toadlet redirectingToadlet = new RedirectToadlet("/new-location");
 		when(toadletContainer.findToadlet(new URI("/redirect-toadlet"))).thenReturn(redirectingToadlet);
 		when(toadletContainer.findToadlet(new URI("/new-location"))).thenReturn(homepageToadlet);
 		ToadletContextImpl.handle(socket, toadletContainer, null, null, null);
@@ -156,21 +153,21 @@ public class ToadletContextImplTest {
 	}
 
 	@Test
-	public void requestWithFullAccessForwardsRequestToPageMakerForParsingAdvancedModeSwitches() throws Exception {
-		setupInputStream("GET / HTTP/1.0\r\n\r\n");
+	public void requestWithFullAccessSetsAdvancedModeWhenQueryParameterIsProvided() throws Exception {
+		setupInputStream("GET /?fproxyAdvancedMode=2 HTTP/1.0\r\n\r\n");
 		when(toadletContainer.isAllowedFullAccess(any())).thenReturn(true);
 		when(toadletContainer.findToadlet(any())).thenReturn(homepageToadlet);
 		ToadletContextImpl.handle(socket, toadletContainer, pageMaker, null, null);
-		verify(pageMaker).parseMode(any(), eq(toadletContainer));
+		verify(toadletContainer).setAdvancedMode(true);
 	}
 
 	@Test
-	public void requestWithoutFullAccessDoesNotForwardRequestToPageMakerForParsingAdvancedModeSwitches() throws Exception {
-		setupInputStream("GET / HTTP/1.0\r\n\r\n");
+	public void requestWithoutFullAccessDoesNotSetAdvancedMode() throws Exception {
+		setupInputStream("GET /?fproxyAdvancedMode=2 HTTP/1.0\r\n\r\n");
 		when(toadletContainer.isAllowedFullAccess(any())).thenReturn(false);
 		when(toadletContainer.findToadlet(any())).thenReturn(homepageToadlet);
 		ToadletContextImpl.handle(socket, toadletContainer, pageMaker, null, null);
-		verify(pageMaker, never()).parseMode(any(), eq(toadletContainer));
+		verify(toadletContainer, never()).setAdvancedMode(anyBoolean());
 	}
 
 	@Test
@@ -331,30 +328,80 @@ public class ToadletContextImplTest {
 	private final Socket socket = mock(Socket.class, RETURNS_DEEP_STUBS);
 	private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 	private final ToadletContainer toadletContainer = mock(ToadletContainer.class, RETURNS_DEEP_STUBS);
-	private final PageMaker pageMaker = mock(PageMaker.class, RETURNS_DEEP_STUBS);
+	private final PageMaker pageMaker = new PageMaker(PageMaker.THEME.CLEAN, null);
 
-	private final Toadlet homepageToadlet = mock(Toadlet.class, RETURNS_DEEP_STUBS);
-	private final Toadlet noOutputToadlet = mock(Toadlet.class, RETURNS_DEEP_STUBS);
+	private final Toadlet homepageToadlet = new HomepageToadlet();
+	private final Toadlet noOutputToadlet = new NoOutputToadlet();
 	private final PostToadlet postToadlet = new PostToadlet();
 
 	{
 		try {
-			when(socket.getOutputStream()).thenReturn(outputStream);
+			doReturn(outputStream).when(socket).getOutputStream();
+			doReturn(null).when(socket).getInetAddress();
 			when(toadletContainer.getBucketFactory()).thenReturn(new ArrayBucketFactory());
-
-			doAnswer(invocation -> {
-				ToadletContext toadletContext = (ToadletContext) invocation.getArguments()[2];
-				toadletContext.sendReplyHeaders(200, "OK", null, "text/plain", 7);
-				toadletContext.writeData(new byte[] { 'G', 'E', 'T', ' ', 'O', 'K', '\n' });
-				return null;
-			}).when(homepageToadlet).handleMethodGET(any(), any(), any());
-			when(homepageToadlet.findSupportedMethods()).thenReturn("GET");
-			when(noOutputToadlet.findSupportedMethods()).thenReturn("GET");
-		} catch (IOException | ToadletContextClosedException | RedirectException e) {
+		} catch (IOException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
+	private static class RedirectToadlet extends Toadlet {
+
+		private final String redirectTarget;
+
+		@Override
+		public void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx) throws RedirectException {
+			throw new RedirectException(URI.create(redirectTarget));
+		}
+
+		@Override
+		public String path() {
+			return "/redirect-toadlet";
+		}
+
+		RedirectToadlet(String redirectTarget) {
+			super(mock(HighLevelSimpleClient.class));
+			this.redirectTarget = redirectTarget;
+		}
+
+	}
+
+	private static class HomepageToadlet extends Toadlet {
+
+		@Override
+		public void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx) throws ToadletContextClosedException, IOException {
+			ctx.sendReplyHeaders(200, "OK", null, "text/plain", 7);
+			ctx.writeData(new byte[]{'G', 'E', 'T', ' ', 'O', 'K', '\n'});
+		}
+
+		@Override
+		public String path() {
+			return "/homepage-toadlet";
+		}
+
+		HomepageToadlet() {
+			super(mock(HighLevelSimpleClient.class));
+		}
+
+	}
+
+	private static class NoOutputToadlet extends Toadlet {
+
+		@Override
+		public void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx) {
+			// no output
+		}
+
+		@Override
+		public String path() {
+			return "/no-output";
+		}
+
+		NoOutputToadlet() {
+			super(mock(HighLevelSimpleClient.class));
+		}
+
+	}
+
 	private static class PostToadlet extends Toadlet {
 
 		@SuppressWarnings("unused")

@Bombe
Copy link
Contributor Author

Bombe commented Feb 8, 2026

Godfuckingdamnit, I fucking hate Gradle. Why the fuck do I have to fight Gradle all the time?! 🤬

Anyway, it’s also possible to update the byte-buddy version to something more recent; latest Mockito version uses 1.17, but 1.18 works fine here, too.

@bertm
Copy link
Contributor

bertm commented Feb 8, 2026

it’s also possible to update the byte-buddy version to something more recent

The reason those inline mocks are appealing is because they allow our tests to effortlessly work around poorly architected code -- but only until the next major class version (which seems to arrive every 6 months nowadays) after which our test compatibility becomes dependent on the next byte-buddy release.

With this in mind, I'd rather minimize our dependency on byte-buddy and avoid inline mocks.

@Bombe
Copy link
Contributor Author

Bombe commented Feb 8, 2026

The reason those inline mocks are appealing is because they allow our tests to effortlessly work around poorly architected code

…of which we have a lot… 😄

-- but only until the next major class version (which seems to arrive every 6 months nowadays) after which our test compatibility becomes dependent on the next byte-buddy release.

Well… yes, and no. I believe the end result still has to run on Java 8, and building/running tests is not something that the average user will do. I would even go as far as to say that if you’re building fred on a vastly more recent version than what we support, you’re on your own. 🙂

With this in mind, I'd rather minimize our dependency on byte-buddy and avoid inline mocks.

Well, as long as we’re using Mockito, byte-buddy is going nowhere. 😄 Generally you are correct, though; Mockito should actually be used sparingly (and I have started reducing my use of Mockito as the go-to solution for mocking) and only for third-party code that you can’t change. Fred consisting of basically 100% public API (of which large parts can’t be changed either) makes this a rather tricky situation. 😄

Thank you for the patch! I will take a look at at and see if I can whip it into something that I can complete agree with, because it’s not yet sitting quite right with me. 🙂

@Bombe Bombe marked this pull request as draft February 25, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants