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..4245159af 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.22.3 + */ + 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());