From b99ab416298b33dd51b7bf8963fec9d4f41a5bb4 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 10 Aug 2026 07:56:11 -0700 Subject: [PATCH 1/2] Do not put texture-less profiles on player heads A PlayerProfile that has a UUID and a name but no textures property makes the server resolve it against the Mojang session server every time the head is shown. On a top ten panel that is ten lookups per open, which quickly returns HTTP 429 and still renders a default skin. HeadGetter created exactly that profile whenever its own texture fetch failed, and handed it to requesters anyway: the "only if the texture is usable" check tested for a null profile, which createProfile never returns. The failed lookup also overwrote any good cached entry, so a single rate limited call cost a working head for the whole cache period and kept the loop running. - HeadCache gains hasTexture() and only calls setOwnerProfile when a skin is actually known, so a failed lookup yields a plain head instead of one the server keeps trying to resolve. - HeadGetter only notifies requesters when the texture is usable, and no longer lets a failed fetch evict a cached head that has one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014HtnKzNE649pBrCdqMm7nw --- .../bentobox/util/heads/HeadCache.java | 28 +++++++++++++++++-- .../bentobox/util/heads/HeadGetter.java | 16 +++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java b/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java index d30472a4a..9da8a29da 100644 --- a/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java +++ b/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java @@ -82,6 +82,30 @@ public HeadCache(String userName, // --------------------------------------------------------------------- + /** + * Checks if this cache holds a usable skin texture. + *

+ * A profile that has a name and a UUID but no texture property is worse than no + * profile at all: the server resolves it against the Mojang session server every time + * the head is shown, which quickly earns an HTTP 429 and still renders a default skin. + * + * @return {@code true} if the cached profile has a skin texture. + * @since 3.23.0 + */ + public boolean hasTexture() + { + try + { + return this.playerProfile != null && this.playerProfile.getTextures().getSkin() != null; + } + catch (Exception e) + { + // Treat an unreadable profile as having no texture. + return false; + } + } + + /** * Returns a new Player head with a cached texture. Be AWARE, usage does not use clone * method. If for some reason item stack is stored directly, then use clone in return @@ -94,8 +118,8 @@ public ItemStack getPlayerHead() ItemStack item = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) item.getItemMeta(); - // Set correct Skull texture - if (meta != null && this.playerProfile != null) + // Set correct Skull texture. Only if the texture is actually known - see hasTexture. + if (meta != null && this.hasTexture()) { try { meta.setOwnerProfile(this.playerProfile); diff --git a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java index 8e31f8bb1..1bf2fa92a 100644 --- a/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java +++ b/src/main/java/world/bentobox/bentobox/util/heads/HeadGetter.java @@ -176,11 +176,21 @@ private void runPlayerHeadGetter() { HeadGetter.createProfile(userName, userId, HeadGetter.getTextureFromUUID(userId))); } - // Save in cache - HeadGetter.cachedHeads.put(userName, cache); + // Save in cache. A failed lookup must not evict a texture we already + // have, otherwise one rate-limited call costs a working head for the + // whole cache period. + HeadCache previous = HeadGetter.cachedHeads.get(userName); + + if (cache.hasTexture() || previous == null || !previous.hasTexture()) { + HeadGetter.cachedHeads.put(userName, cache); + } else { + cache = previous; + } // Tell requesters the head came in, but only if the texture is usable. - if (cache.playerProfile != null && HeadGetter.headRequesters.containsKey(userName)) { + // Handing out a profile with no texture makes the server look it up at + // Mojang every time the head is shown, which ends in HTTP 429s. + if (cache.hasTexture() && HeadGetter.headRequesters.containsKey(userName)) { for (HeadRequester req : HeadGetter.headRequesters.get(userName)) { elementEntry.getValue().setHead(cache.getPlayerHead()); From 7278da1e7b143da49f5219397353b0ea20df41c9 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 10 Aug 2026 08:02:29 -0700 Subject: [PATCH 2/2] Update Javadoc version in HeadCache.java --- src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java b/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java index 9da8a29da..4245159af 100644 --- a/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java +++ b/src/main/java/world/bentobox/bentobox/util/heads/HeadCache.java @@ -90,7 +90,7 @@ public HeadCache(String userName, * the head is shown, which quickly earns an HTTP 429 and still renders a default skin. * * @return {@code true} if the cached profile has a skin texture. - * @since 3.23.0 + * @since 3.22.3 */ public boolean hasTexture() {