Skip to content

Do not put texture-less profiles on player heads - #3063

Merged
tastybento merged 2 commits into
developfrom
fix/head-texture-mojang-429
Aug 10, 2026
Merged

Do not put texture-less profiles on player heads#3063
tastybento merged 2 commits into
developfrom
fix/head-texture-mojang-429

Conversation

@tastybento

Copy link
Copy Markdown
Member

Problem

Opening a panel full of player heads (a top ten, for example) can spam the console with:

[WARN]: Couldn't look up profile properties for c44d4387-e438-4880-bb8a-4e267340feda
MinecraftClientHttpException[type=HTTP_ERROR, status=429, response=null]
	at com.mojang.authlib.minecraft.client.MinecraftClient.buildHttpException(MinecraftClient.java:160)
	...
	at com.destroystokyo.paper.profile.PaperMinecraftSessionService.fetchProfile(PaperMinecraftSessionService.java:40)
	at org.bukkit.craftbukkit.profile.CraftPlayerProfile.getUpdatedProfile(CraftPlayerProfile.java:157)

Those lookups are made by the server, not by us. A PlayerProfile that carries a UUID and a name but no textures property is incomplete, so the server resolves it against the Mojang session server every time the head is shown — see PaperMC/Paper#13727 and the discussion on PaperMC/Paper#13692. Ten heads in a panel is ten lookups per open, which reaches HTTP 429 quickly and still renders a default skin at the end of it.

HeadGetter produces precisely that profile whenever its own texture fetch comes back empty: createProfile() always returns a profile, with or without a skin, and getTextureFromUUID() swallows every failure and returns null. It then hands the result to requesters regardless, because the check guarding delivery —

// Tell requesters the head came in, but only if the texture is usable.
if (cache.playerProfile != null && HeadGetter.headRequesters.containsKey(userName)) {

— tests for a null profile, which createProfile() never returns. The comment describes an intent the code never implemented.

The failed lookup was also written straight over the cache, so one rate-limited call replaced a working head with a broken one for the whole cache period. Since the broken profile then triggers a fresh server-side lookup on every panel open, the failure sustains itself.

Changes

  • HeadCache gains hasTexture(), and getPlayerHead() only calls setOwnerProfile() when a skin is actually known. With no profile attached there is nothing for the server to resolve, so a failed lookup degrades to a plain head instead of one that keeps hitting Mojang.
  • HeadGetter only notifies requesters when the texture is usable, making the existing comment true, and no longer lets a failed fetch evict a cached head that already has a texture.

No API is removed; hasTexture() is additive.

Testing

./gradlew compileJava is clean and PanelTest, which exercises HeadGetter, passes. Other *PanelTest classes in the tree fail identically before and after this change (UnfinishedStubbingException during static init), unrelated to heads.

Note for server owners

Setting use-cache-server: true is still worth doing independently. The defaults (heads-per-call: 9, ticks-between-calls: 10) allow roughly 18 direct requests per second to sessionserver.mojang.com from BentoBox's own fetcher, which is enough to trip the rate limit that starts this cycle.

🤖 Generated with Claude Code

https://claude.ai/code/session_014HtnKzNE649pBrCdqMm7nw

tastybento and others added 2 commits August 10, 2026 07:56
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HtnKzNE649pBrCdqMm7nw
@tastybento
tastybento merged commit 8b3f0fe into develop Aug 10, 2026
1 check passed
@tastybento
tastybento deleted the fix/head-texture-mojang-429 branch August 10, 2026 15:02
@sonarqubecloud

Copy link
Copy Markdown

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