Skip to content

v2.2.4#14

Open
byteful wants to merge 11 commits into
mainfrom
dev
Open

v2.2.4#14
byteful wants to merge 11 commits into
mainfrom
dev

Conversation

@byteful

@byteful byteful commented May 21, 2026

Copy link
Copy Markdown
Member

Confidence Score: 3/5

This should be fixed before merging.

  • Realtime active-staff handling is incomplete.
  • Staff state can remain cached after a staff member is removed or no longer active.
  • The new websocket path now carries core moderation state, so stale staff data can affect visible behavior.

Focus on MinecraftRealtimeClient.java and SyncService.java.

Important Files Changed

Filename Overview
core/src/main/java/gg/modl/minecraft/core/realtime/MinecraftRealtimeClient.java Adds websocket domain event handling and topic subscriptions for realtime sync.
core/src/main/java/gg/modl/minecraft/core/service/sync/SyncService.java Reworks sync around realtime baseline fetches, fallback fetches, and public push-apply methods.
core/src/main/java/gg/modl/minecraft/core/impl/http/ModlHttpClientV3Impl.java Introduces the default protobuf V3 HTTP client and delegates a few legacy endpoints to V2.

Comments Outside Diff (1)

  1. core/src/main/java/gg/modl/minecraft/core/impl/http/ModlHttpClientV2Impl.java, line 824-827 (link)

    P1 security Redact API key

    When debug mode is enabled this logs the full header map, including X-API-Key. Server logs are commonly shared for support, so this can expose the API key for every request. Log a redacted copy of the headers instead of the raw request headers.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: core/src/main/java/gg/modl/minecraft/core/impl/http/ModlHttpClientV2Impl.java
    Line: 824-827
    
    Comment:
    **Redact API key**
    
    When debug mode is enabled this logs the full header map, including `X-API-Key`. Server logs are commonly shared for support, so this can expose the API key for every request. Log a redacted copy of the headers instead of the raw request headers.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
core/src/main/java/gg/modl/minecraft/core/realtime/MinecraftRealtimeClient.java:474-484
**Active staff unsubscribed**

This allowlist filters the startup topics before `buildSubscribe()` sends the subscription, but it omits the active-staff topic even though this client now handles `ACTIVE_STAFF_PUSH`. If the backend advertises active staff updates, they are dropped by `parseTopics()` and the websocket never subscribes to them. With the periodic poll removed while realtime is connected, staff cache and permission state can stay stale until a reconnect or baseline fetch.

### Issue 2 of 2
core/src/main/java/gg/modl/minecraft/core/service/sync/SyncService.java:609-610
**Staff removals persist**

The baseline sync treats `activeStaffMembers` as the authoritative full set and evicts cached staff profiles that are no longer present, but the realtime push path only updates each pushed member. When a staff member is removed, loses staff state, or disappears from the active set while the websocket is connected, this method has no matching eviction step, so `profile.getStaffMember()` and cached staff permissions can remain set until the next reconnect baseline.

Reviews (2): Last reviewed commit: "realtime push consume + proto v3 client" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

  • Rule used - THE CENTRAL RULE everything else serves: maintain ... (source)

byteful and others added 8 commits May 2, 2026 01:29
# Conflicts:
#	bridge-core/src/main/java/gg/modl/minecraft/bridge/reporter/ModlBackendReplayUploader.java
#	platforms/spigot/src/main/java/gg/modl/minecraft/spigot/bridge/BridgeComponent.java
#	platforms/spigot/src/test/java/gg/modl/minecraft/spigot/bridge/BridgeComponentTest.java
@byteful byteful marked this pull request as ready for review May 22, 2026 04:19
@byteful

byteful commented May 22, 2026

Copy link
Copy Markdown
Member Author

@greptileai

Comment on lines +278 to +280
static boolean shouldReconnectAfterClose(int code) {
return code != 1008 && code != 1013;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reconnect stops permanently

This treats close code 1013 as a terminal close. That code means the server is temporarily unavailable and asks clients to try again later, so a backend deploy or overload response will set running = false in handleClose and realtime invalidations will never resume until the plugin restarts. This should reconnect with backoff instead of stopping the client.

Suggested change
static boolean shouldReconnectAfterClose(int code) {
return code != 1008 && code != 1013;
}
static boolean shouldReconnectAfterClose(int code) {
return code != 1008;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/src/main/java/gg/modl/minecraft/core/realtime/MinecraftRealtimeClient.java
Line: 278-280

Comment:
**Reconnect stops permanently**

This treats close code `1013` as a terminal close. That code means the server is temporarily unavailable and asks clients to try again later, so a backend deploy or overload response will set `running = false` in `handleClose` and realtime invalidations will never resume until the plugin restarts. This should reconnect with backoff instead of stopping the client.

```suggestion
    static boolean shouldReconnectAfterClose(int code) {
        return code != 1008;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +88 to +92
if (!localEnabled || startupResponse == null) return false;
if (!Boolean.TRUE.equals(startupResponse.getRealtimeEnabled())) return false;
String url = startupResponse.getRealtimeUrl();
if (url == null || url.trim().isEmpty()) return false;
return !parseTopics(startupResponse.getRealtimeTopics(), normalize(startupResponse.getServerInstanceId())).isEmpty();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Validate realtime scheme

canStart accepts any non-empty realtime URL, but connectOnce passes that URI directly to WebSocketClient. If startup returns an https:// or http:// endpoint by mistake, the realtime client starts and then fails/retries forever instead of rejecting the startup payload and staying cleanly on polling. Validate ws/wss here before starting the client.

Prompt To Fix With AI
This is a comment left during a code review.
Path: core/src/main/java/gg/modl/minecraft/core/realtime/MinecraftRealtimeClient.java
Line: 88-92

Comment:
**Validate realtime scheme**

`canStart` accepts any non-empty realtime URL, but `connectOnce` passes that URI directly to `WebSocketClient`. If startup returns an `https://` or `http://` endpoint by mistake, the realtime client starts and then fails/retries forever instead of rejecting the startup payload and staying cleanly on polling. Validate `ws`/`wss` here before starting the client.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +30 to +39
@SuppressWarnings("unchecked")
public static Map<String, Object> loadMap(Path file) throws IOException {
try (InputStream input = Files.newInputStream(file)) {
Object loaded = new Yaml().load(input);
if (loaded == null) {
return Collections.emptyMap();
}
return (Map<String, Object>) loaded;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Malformed config crashes

This unchecked cast lets a scalar or list YAML root throw ClassCastException. BridgeConfig.load is called from bridge startup under a catch that only handles IOException, so a malformed bridge-config.yml can escape the fallback path and abort bridge initialization instead of logging and using defaults. Check the loaded root type and return an empty map or throw IOException for unsupported roots.

Suggested change
@SuppressWarnings("unchecked")
public static Map<String, Object> loadMap(Path file) throws IOException {
try (InputStream input = Files.newInputStream(file)) {
Object loaded = new Yaml().load(input);
if (loaded == null) {
return Collections.emptyMap();
}
return (Map<String, Object>) loaded;
}
}
@SuppressWarnings("unchecked")
public static Map<String, Object> loadMap(Path file) throws IOException {
try (InputStream input = Files.newInputStream(file)) {
Object loaded = new Yaml().load(input);
if (loaded == null) {
return Collections.emptyMap();
}
if (!(loaded instanceof Map)) {
throw new IOException("YAML root must be a map: " + file);
}
return (Map<String, Object>) loaded;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: bridge-core/src/main/java/gg/modl/minecraft/bridge/resource/BridgeYamlResource.java
Line: 30-39

Comment:
**Malformed config crashes**

This unchecked cast lets a scalar or list YAML root throw `ClassCastException`. `BridgeConfig.load` is called from bridge startup under a catch that only handles `IOException`, so a malformed `bridge-config.yml` can escape the fallback path and abort bridge initialization instead of logging and using defaults. Check the loaded root type and return an empty map or throw `IOException` for unsupported roots.

```suggestion
    @SuppressWarnings("unchecked")
    public static Map<String, Object> loadMap(Path file) throws IOException {
        try (InputStream input = Files.newInputStream(file)) {
            Object loaded = new Yaml().load(input);
            if (loaded == null) {
                return Collections.emptyMap();
            }
            if (!(loaded instanceof Map)) {
                throw new IOException("YAML root must be a map: " + file);
            }
            return (Map<String, Object>) loaded;
        }
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +195 to +196
new ThreadPoolExecutor.CallerRunsPolicy()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Caller thread blocks

The lookup pool uses a bounded queue with CallerRunsPolicy, so once two lookups are running and 64 are queued, the login caller performs lookupRemote itself. That method can spend up to five seconds on connect and five seconds on read, which means a login event thread or login executor worker can block on public IP enrichment instead of failing that optional enrichment fast. Under a join burst or slow ipwho.is, login verification can stall even though this code was moved behind an executor.

Suggested change
new ThreadPoolExecutor.CallerRunsPolicy()
);
new ThreadPoolExecutor.AbortPolicy()
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/src/main/java/gg/modl/minecraft/core/util/IpApiClient.java
Line: 195-196

Comment:
**Caller thread blocks**

The lookup pool uses a bounded queue with `CallerRunsPolicy`, so once two lookups are running and 64 are queued, the login caller performs `lookupRemote` itself. That method can spend up to five seconds on connect and five seconds on read, which means a login event thread or login executor worker can block on public IP enrichment instead of failing that optional enrichment fast. Under a join burst or slow `ipwho.is`, login verification can stall even though this code was moved behind an executor.

```suggestion
                new ThreadPoolExecutor.AbortPolicy()
        );
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 89 to +135
@@ -93,13 +103,15 @@ private void onPlayerJoin(ServerPlayerEntity player) {
PlayerLoginRequest request = new PlayerLoginRequest(
uuid.toString(), playerName,
ipAddress, skinHash, platform.getServerName(), ipInfo);
request.setServerInstanceId(StartupClient.getServerInstanceId());
return new Object[]{request, ipInfo, skinHash};
}).thenCompose(data -> {
PlayerLoginRequest request = (PlayerLoginRequest) data[0];
@SuppressWarnings("unchecked")
Map<String, Object> ipInfo = (Map<String, Object>) data[1];
String skinHash = (String) data[2];
return getHttpClient().playerLogin(request)
.orTimeout(LOGIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.thenAccept(response -> {
loginCache.cacheLoginResult(uuid, response, ipInfo, skinHash);
ListenerHelper.handlePendingIpLookups(
@@ -114,39 +126,45 @@ private void onPlayerJoin(ServerPlayerEntity player) {
LoginHandler.LoginResult errorResult = LoginHandler.handleLoginError(error);
if (errorResult instanceof LoginHandler.LoginResult.Denied) {
LoginHandler.LoginResult.Denied denied = (LoginHandler.LoginResult.Denied) errorResult;
server.execute(() -> platform.kickPlayer(
platform.getAbstractPlayer(uuid, false), denied.getMessage()));
kickForLoginFailure(uuid, denied.getMessage());
} else {
completeJoin(uuid, playerName, null);
kickForLoginFailure(uuid, "Unable to verify ban status. Login temporarily restricted for safety.");
}
return null;
}).get(LOGIN_TIMEOUT_SECONDS, java.util.concurrent.TimeUnit.SECONDS);
} catch (Exception e) {
platform.getLogger().warning("Async login timed out for " + playerName + ": " + e.getMessage());
completeJoin(uuid, playerName, null);
}
});
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Login work escapes executor

The executor task only creates the CompletableFuture chain and then returns. The IP lookup, Mojang lookup, backend login request, and kick/complete callbacks keep running on other executors and are not tracked by this listener, so the new queue and shutdown() do not limit or cancel active login checks. A burst of joins can still create many concurrent backend login requests, and a plugin shutdown can still leave callbacks kicking or completing players through disabled services. Keep the whole login check inside the executor task, or store and cancel the returned future on shutdown.

Prompt To Fix With AI
This is a comment left during a code review.
Path: platforms/fabric-121/src/main/java/gg/modl/minecraft/fabric/v1_21_1/FabricListener.java
Line: 89-135

Comment:
**Login work escapes executor**

The executor task only creates the `CompletableFuture` chain and then returns. The IP lookup, Mojang lookup, backend login request, and kick/complete callbacks keep running on other executors and are not tracked by this listener, so the new queue and `shutdown()` do not limit or cancel active login checks. A burst of joins can still create many concurrent backend login requests, and a plugin shutdown can still leave callbacks kicking or completing players through disabled services. Keep the whole login check inside the executor task, or store and cancel the returned future on shutdown.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +474 to +484
private static Set<Topic> allowedStartupTopics() {
Set<Topic> topics = new HashSet<>();
topics.add(Topic.TOPIC_MINECRAFT_PERMISSIONS);
topics.add(Topic.TOPIC_MINECRAFT_PUNISHMENT_TYPES);
topics.add(Topic.TOPIC_MINECRAFT_STAFF_NOTIFICATIONS);
topics.add(Topic.TOPIC_MINECRAFT_PUNISHMENTS);
topics.add(Topic.TOPIC_MINECRAFT_PLAYER_NOTIFICATIONS);
topics.add(Topic.TOPIC_MINECRAFT_STAFF_2FA);
topics.add(Topic.TOPIC_MINECRAFT_MIGRATION_TASKS);
topics.add(Topic.TOPIC_MINECRAFT_STAT_WIPES);
return topics;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Active staff unsubscribed

This allowlist filters the startup topics before buildSubscribe() sends the subscription, but it omits the active-staff topic even though this client now handles ACTIVE_STAFF_PUSH. If the backend advertises active staff updates, they are dropped by parseTopics() and the websocket never subscribes to them. With the periodic poll removed while realtime is connected, staff cache and permission state can stay stale until a reconnect or baseline fetch.

Rule Used: THE CENTRAL RULE everything else serves: maintain ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: core/src/main/java/gg/modl/minecraft/core/realtime/MinecraftRealtimeClient.java
Line: 474-484

Comment:
**Active staff unsubscribed**

This allowlist filters the startup topics before `buildSubscribe()` sends the subscription, but it omits the active-staff topic even though this client now handles `ACTIVE_STAFF_PUSH`. If the backend advertises active staff updates, they are dropped by `parseTopics()` and the websocket never subscribes to them. With the periodic poll removed while realtime is connected, staff cache and permission state can stay stale until a reconnect or baseline fetch.

**Rule Used:** THE CENTRAL RULE everything else serves: maintain ... ([source](https://app.greptile.com/modl-gg/-/custom-context?memory=21719055-65fa-440f-b434-eba8d358d917))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +609 to +610
public void applyActiveStaffMember(SyncResponse.ActiveStaffMember staffMember) {
processActiveStaffMember(staffMember);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Staff removals persist

The baseline sync treats activeStaffMembers as the authoritative full set and evicts cached staff profiles that are no longer present, but the realtime push path only updates each pushed member. When a staff member is removed, loses staff state, or disappears from the active set while the websocket is connected, this method has no matching eviction step, so profile.getStaffMember() and cached staff permissions can remain set until the next reconnect baseline.

Rule Used: THE CENTRAL RULE everything else serves: maintain ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: core/src/main/java/gg/modl/minecraft/core/service/sync/SyncService.java
Line: 609-610

Comment:
**Staff removals persist**

The baseline sync treats `activeStaffMembers` as the authoritative full set and evicts cached staff profiles that are no longer present, but the realtime push path only updates each pushed member. When a staff member is removed, loses staff state, or disappears from the active set while the websocket is connected, this method has no matching eviction step, so `profile.getStaffMember()` and cached staff permissions can remain set until the next reconnect baseline.

**Rule Used:** THE CENTRAL RULE everything else serves: maintain ... ([source](https://app.greptile.com/modl-gg/-/custom-context?memory=21719055-65fa-440f-b434-eba8d358d917))

How can I resolve this? If you propose a fix, please make it concise.

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.

1 participant