From b737aa46fc20c252a74b21a59c681b55948d08d7 Mon Sep 17 00:00:00 2001 From: Dev10-sys Date: Fri, 24 Apr 2026 20:46:36 +0530 Subject: [PATCH 1/3] fix: handle expired filters in event subscriptions to prevent 'filter not found' error (#1998) Signed-off-by: Dev10-sys --- CHANGELOG.md | 4 +- .../web3j/protocol/core/filters/Filter.java | 34 ++++---- .../test/java/org/web3j/ens/NameHashTest.java | 5 +- .../core/filters/FilterRecoveryTest.java | 79 +++++++++++++++++++ 4 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ca7d6979..2168638b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -* Replace raw usage of `EthLog.LogResult` with parameterized type to improve type safety (#2252) -(https://github.com/LFDT-web3j/web3j/pull/2254) +* Replace raw usage of `EthLog.LogResult` with parameterized type to improve type safety (#2252) (https://github.com/LFDT-web3j/web3j/pull/2254) +* Fix handling of expired or missing filters in event subscriptions to prevent "filter not found" errors (#1998) ### Features diff --git a/core/src/main/java/org/web3j/protocol/core/filters/Filter.java b/core/src/main/java/org/web3j/protocol/core/filters/Filter.java index 1ceb2790e7..f7c4405cc1 100644 --- a/core/src/main/java/org/web3j/protocol/core/filters/Filter.java +++ b/core/src/main/java/org/web3j/protocol/core/filters/Filter.java @@ -120,11 +120,11 @@ private void getInitialFilterLogs() { } if (ethLog.hasError()) { - throwException(ethLog.getError()); + handleError(ethLog.getError()); + } else { + process(ethLog.getLogs()); } - process(ethLog.getLogs()); - } catch (IOException e) { throwException(e); } @@ -138,23 +138,23 @@ private void pollFilter(EthFilter ethFilter) { throwException(e); } if (ethLog.hasError()) { - Error error = ethLog.getError(); - String message = error.getMessage(); - switch (error.getCode()) { - case RpcErrors.FILTER_NOT_FOUND: - reinstallFilter(); - break; - default: - if (Pattern.compile(FILTER_NOT_FOUND_PATTERN).matcher(message).find()) - reinstallFilter(); - else throwException(error); - break; - } + handleError(ethLog.getError()); } else { process(ethLog.getLogs()); } } + private void handleError(Error error) { + if (RpcErrors.FILTER_NOT_FOUND == error.getCode()) { + reinstallFilter(); + } else if (error.getMessage() != null + && Pattern.compile(FILTER_NOT_FOUND_PATTERN).matcher(error.getMessage()).find()) { + reinstallFilter(); + } else { + throwException(error); + } + } + protected abstract EthFilter sendRequest() throws IOException; protected abstract void process(List> logResults); @@ -163,7 +163,9 @@ private void reinstallFilter() { log.warn( "Previously installed filter has not been found, trying to re-install. Filter id: {}", filterId); - schedule.cancel(false); + if (schedule != null) { + schedule.cancel(false); + } this.run(scheduledExecutorService, blockTime); } diff --git a/core/src/test/java/org/web3j/ens/NameHashTest.java b/core/src/test/java/org/web3j/ens/NameHashTest.java index baa1aa1059..c4cb2b82d7 100644 --- a/core/src/test/java/org/web3j/ens/NameHashTest.java +++ b/core/src/test/java/org/web3j/ens/NameHashTest.java @@ -72,7 +72,10 @@ void testNormalise() { assertEquals(normalise("Obb.at"), ("obb.at")); assertEquals(normalise("TESTER.eth"), ("tester.eth")); assertEquals(normalise("test\u200btest.com"), ("testtest.com")); - assertEquals(normalise("hyph-‐‑‒–—―⁃−⎯⏤﹘e⸺n⸻s.eth"), ("hyph------------e--n---s.eth")); + assertEquals( + normalise( + "hyph-\u2010\u2011\u2012\u2013\u2014\u2015\u2043\u2212\u23af\u23e4\ufe58e\u2e3an\u2e3bs.eth"), + ("hyph------------e--n---s.eth")); } @Test diff --git a/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java b/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java new file mode 100644 index 0000000000..18e02d93db --- /dev/null +++ b/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java @@ -0,0 +1,79 @@ +package org.web3j.protocol.core.filters; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.web3j.protocol.ObjectMapperFactory; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.Web3jService; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.EthLog; +import org.web3j.protocol.core.methods.response.EthUninstallFilter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FilterRecoveryTest { + + private Web3jService web3jService; + private Web3j web3j; + private final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(); + private final ScheduledExecutorService scheduledExecutorService = + Executors.newSingleThreadScheduledExecutor(); + + @BeforeEach + public void setUp() { + web3jService = mock(Web3jService.class); + web3j = Web3j.build(web3jService, 1000, scheduledExecutorService); + } + + @Test + public void testFilterNotFoundRecoveryInInitialLogs() throws Exception { + org.web3j.protocol.core.methods.response.EthFilter ethFilterResponse = + objectMapper.readValue( + "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x1\"}", + org.web3j.protocol.core.methods.response.EthFilter.class); + + EthLog notFoundError = objectMapper.readValue( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32000,\"message\":\"filter not found\"}}", + EthLog.class); + + EthLog successLog = objectMapper.readValue( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}", EthLog.class); + + EthUninstallFilter ethUninstallFilter = + objectMapper.readValue( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":true}", EthUninstallFilter.class); + + // Mock ethNewFilter + when(web3jService.send(any(Request.class), eq(org.web3j.protocol.core.methods.response.EthFilter.class))) + .thenReturn(ethFilterResponse); + + // First call to ethGetFilterLogs (sent via service) returns error, subsequent returns success + when(web3jService.send(any(Request.class), eq(EthLog.class))) + .thenReturn(notFoundError) + .thenReturn(successLog); + + when(web3jService.send(any(Request.class), eq(EthUninstallFilter.class))) + .thenReturn(ethUninstallFilter); + + LogFilter filter = new LogFilter(web3j, log -> {}, new EthFilter()); + + // This should not throw FilterException anymore + filter.run(scheduledExecutorService, 100); + + // Verification: Service should have received ethNewFilter request at least twice + // (one for initial run, one for re-install after error) + verify(web3jService, atLeast(2)).send(any(Request.class), eq(org.web3j.protocol.core.methods.response.EthFilter.class)); + filter.cancel(); + } +} From ef260e582f2b21c0592cc3e4cecc277cf5c78053 Mon Sep 17 00:00:00 2001 From: Dev10-sys Date: Sat, 25 Apr 2026 00:35:40 +0530 Subject: [PATCH 2/3] fix: handle expired filters in event subscriptions to prevent 'filter not found' error (#1998) Signed-off-by: Dev10-sys --- CHANGELOG.md | 3 +- .../web3j/protocol/core/filters/Filter.java | 23 +++++- .../core/filters/FilterRecoveryTest.java | 74 +++++++++++++++---- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2168638b83..8951e97f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Bug Fixes -* Replace raw usage of `EthLog.LogResult` with parameterized type to improve type safety (#2252) (https://github.com/LFDT-web3j/web3j/pull/2254) +* Replace raw usage of `EthLog.LogResult` with parameterized type to improve type safety (#2252) +(https://github.com/LFDT-web3j/web3j/pull/2254) * Fix handling of expired or missing filters in event subscriptions to prevent "filter not found" errors (#1998) ### Features diff --git a/core/src/main/java/org/web3j/protocol/core/filters/Filter.java b/core/src/main/java/org/web3j/protocol/core/filters/Filter.java index f7c4405cc1..a771a305fa 100644 --- a/core/src/main/java/org/web3j/protocol/core/filters/Filter.java +++ b/core/src/main/java/org/web3j/protocol/core/filters/Filter.java @@ -50,7 +50,11 @@ public abstract class Filter { private long blockTime; - private static final String FILTER_NOT_FOUND_PATTERN = "(?i)\\bfilter\\s+not\\s+found\\b"; + private static final Pattern FILTER_NOT_FOUND_REGEX = + Pattern.compile("(?i)\\bfilter\\s+not\\s+found\\b"); + + private int reinstallRetries = 0; + private static final int MAX_REINSTALL_RETRIES = 3; public Filter(Web3j web3j, Callback callback) { this.web3j = web3j; @@ -122,6 +126,7 @@ private void getInitialFilterLogs() { if (ethLog.hasError()) { handleError(ethLog.getError()); } else { + reinstallRetries = 0; process(ethLog.getLogs()); } @@ -140,6 +145,7 @@ private void pollFilter(EthFilter ethFilter) { if (ethLog.hasError()) { handleError(ethLog.getError()); } else { + reinstallRetries = 0; process(ethLog.getLogs()); } } @@ -148,7 +154,7 @@ private void handleError(Error error) { if (RpcErrors.FILTER_NOT_FOUND == error.getCode()) { reinstallFilter(); } else if (error.getMessage() != null - && Pattern.compile(FILTER_NOT_FOUND_PATTERN).matcher(error.getMessage()).find()) { + && FILTER_NOT_FOUND_REGEX.matcher(error.getMessage()).find()) { reinstallFilter(); } else { throwException(error); @@ -160,9 +166,18 @@ private void handleError(Error error) { protected abstract void process(List> logResults); private void reinstallFilter() { + if (reinstallRetries >= MAX_REINSTALL_RETRIES) { + log.error( + "Exceeded maximum number of filter re-installations ({})", + MAX_REINSTALL_RETRIES); + throw new FilterException("Exceeded maximum number of filter re-installations"); + } + reinstallRetries++; + log.warn( - "Previously installed filter has not been found, trying to re-install. Filter id: {}", - filterId); + "Previously installed filter has not been found, trying to re-install. Filter id: {}, retry: {}", + filterId, + reinstallRetries); if (schedule != null) { schedule.cancel(false); } diff --git a/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java b/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java index 18e02d93db..75b9e28652 100644 --- a/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java +++ b/core/src/test/java/org/web3j/protocol/core/filters/FilterRecoveryTest.java @@ -15,10 +15,13 @@ import org.web3j.protocol.core.methods.response.EthLog; import org.web3j.protocol.core.methods.response.EthUninstallFilter; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,31 +41,34 @@ public void setUp() { @Test public void testFilterNotFoundRecoveryInInitialLogs() throws Exception { - org.web3j.protocol.core.methods.response.EthFilter ethFilterResponse = + org.web3j.protocol.core.methods.response.EthFilter ethFilterResponse = objectMapper.readValue( - "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x1\"}", - org.web3j.protocol.core.methods.response.EthFilter.class); + "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x1\"}", + org.web3j.protocol.core.methods.response.EthFilter.class); - EthLog notFoundError = objectMapper.readValue( - "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32000,\"message\":\"filter not found\"}}", - EthLog.class); + EthLog notFoundError = + objectMapper.readValue( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32000,\"message\":\"filter not found\"}}", + EthLog.class); - EthLog successLog = objectMapper.readValue( - "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}", EthLog.class); + EthLog successLog = + objectMapper.readValue("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}", EthLog.class); EthUninstallFilter ethUninstallFilter = objectMapper.readValue( "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":true}", EthUninstallFilter.class); // Mock ethNewFilter - when(web3jService.send(any(Request.class), eq(org.web3j.protocol.core.methods.response.EthFilter.class))) + when(web3jService.send( + any(Request.class), + eq(org.web3j.protocol.core.methods.response.EthFilter.class))) .thenReturn(ethFilterResponse); - // First call to ethGetFilterLogs (sent via service) returns error, subsequent returns success + // First call to ethGetFilterLogs returns error, subsequent returns success when(web3jService.send(any(Request.class), eq(EthLog.class))) - .thenReturn(notFoundError) - .thenReturn(successLog); - + .thenReturn(notFoundError) + .thenReturn(successLog); + when(web3jService.send(any(Request.class), eq(EthUninstallFilter.class))) .thenReturn(ethUninstallFilter); @@ -72,8 +78,46 @@ public void testFilterNotFoundRecoveryInInitialLogs() throws Exception { filter.run(scheduledExecutorService, 100); // Verification: Service should have received ethNewFilter request at least twice - // (one for initial run, one for re-install after error) - verify(web3jService, atLeast(2)).send(any(Request.class), eq(org.web3j.protocol.core.methods.response.EthFilter.class)); + verify(web3jService, atLeast(2)) + .send( + any(Request.class), + eq(org.web3j.protocol.core.methods.response.EthFilter.class)); filter.cancel(); } + + @Test + public void testFilterNotFoundExceedsRetryLimit() throws Exception { + org.web3j.protocol.core.methods.response.EthFilter ethFilterResponse = + objectMapper.readValue( + "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x1\"}", + org.web3j.protocol.core.methods.response.EthFilter.class); + + EthLog notFoundError = + objectMapper.readValue( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32000,\"message\":\"filter not found\"}}", + EthLog.class); + + when(web3jService.send(any(Request.class), eq(EthLog.class))).thenReturn(notFoundError); + + when(web3jService.send( + any(Request.class), + eq(org.web3j.protocol.core.methods.response.EthFilter.class))) + .thenReturn(ethFilterResponse); + + LogFilter filter = new LogFilter(web3j, log -> {}, new EthFilter()); + + try { + filter.run(scheduledExecutorService, 100); + fail("Should have thrown FilterException due to retry limit"); + } catch (FilterException e) { + assertTrue( + e.getMessage().contains("Exceeded maximum number of filter re-installations")); + } + + // Verify it tried 4 times (1 initial + 3 retries) + verify(web3jService, times(4)) + .send( + any(Request.class), + eq(org.web3j.protocol.core.methods.response.EthFilter.class)); + } } From 825759fe1ef6b976396596288e1dfc292fdda5f4 Mon Sep 17 00:00:00 2001 From: Dev10-sys Date: Sat, 25 Apr 2026 13:34:46 +0530 Subject: [PATCH 3/3] fix: handle expired filters in event subscriptions to prevent 'filter not found' error (#1998) Signed-off-by: Dev10-sys --- core/src/test/java/org/web3j/ens/NameHashTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/test/java/org/web3j/ens/NameHashTest.java b/core/src/test/java/org/web3j/ens/NameHashTest.java index c4cb2b82d7..baa1aa1059 100644 --- a/core/src/test/java/org/web3j/ens/NameHashTest.java +++ b/core/src/test/java/org/web3j/ens/NameHashTest.java @@ -72,10 +72,7 @@ void testNormalise() { assertEquals(normalise("Obb.at"), ("obb.at")); assertEquals(normalise("TESTER.eth"), ("tester.eth")); assertEquals(normalise("test\u200btest.com"), ("testtest.com")); - assertEquals( - normalise( - "hyph-\u2010\u2011\u2012\u2013\u2014\u2015\u2043\u2212\u23af\u23e4\ufe58e\u2e3an\u2e3bs.eth"), - ("hyph------------e--n---s.eth")); + assertEquals(normalise("hyph-‐‑‒–—―⁃−⎯⏤﹘e⸺n⸻s.eth"), ("hyph------------e--n---s.eth")); } @Test