ben-manes/caffeine · error · InvalidObjectException

Proxy required

Error message

Proxy required

What it means

UnboundedLocalCache's CacheView throws InvalidObjectException("Proxy required") in readObject: the unbounded cache (Caffeine.newBuilder().build() with no size/expiry limits) serializes through a dedicated SerializationProxy emitted by writeReplace, carrying only the stats flag and removal listener. Direct deserialization of the view class is refused to prevent half-constructed caches.

Source

Thrown at caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java:1226

      cache = new UnboundedLocalCache<>(builder, /* isAsync= */ false);
    }

    @Override
    public final UnboundedLocalCache<K, V> cache() {
      return cache;
    }

    @Override
    public final Policy<K, V> policy() {
      if (policy == null) {
        Function<@Nullable V, @Nullable V> identity = v -> v;
        policy = new UnboundedPolicy<>(cache, identity);
      }
      return policy;
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
      throw new InvalidObjectException("Proxy required");
    }

    Object writeReplace() {
      var proxy = new SerializationProxy<K, V>();
      proxy.isRecordingStats = cache.isRecordingStats;
      proxy.removalListener = cache.removalListener;
      return proxy;
    }
  }

  /** An eviction policy that supports no bounding. */
  static final class UnboundedPolicy<K, V> implements Policy<K, V> {
    final Function<@Nullable V, @Nullable V> transformer;
    final UnboundedLocalCache<K, V> cache;

    UnboundedPolicy(UnboundedLocalCache<K, V> cache,
        Function<@Nullable V, @Nullable V> transformer) {
      this.transformer = transformer;

View on GitHub (pinned to 9da6581ee3)

Solutions

  1. Serialize the Cache object returned from Caffeine.newBuilder().build() so writeReplace runs
  2. Enable writeReplace-aware mode in your serialization framework or exclude Caffeine internals
  3. For durability or transport, write cache.asMap() entries and rebuild the cache with the same settings on read

Example fix

// before
out.writeObject(unboundedCacheView); // internal view
in.readObject(); // InvalidObjectException: Proxy required

// after
out.writeObject(cache); // UnboundedLocalCache proxy path
Cache<K, V> restored = (Cache<K, V>) in.readObject();
Defensive patterns

Strategy: validation

Validate before calling

// Serialize only the builder-produced cache:
Cache<K, V> cache = Caffeine.newBuilder().build(); // unbounded
out.writeObject(cache);
// When in doubt, snapshot entries instead:
Map<K, V> snapshot = Map.copyOf(cache.asMap());
out.writeObject(snapshot);

Try / catch

try {
  Cache<K, V> restored = (Cache<K, V>) in.readObject();
} catch (InvalidObjectException e) {
  if ("Proxy required".equals(e.getMessage())) {
    // Writer serialized an internal view: fall back to entry-based restore
    Map<K, V> entries = readSnapshot();
    Cache<K, V> restored = Caffeine.newBuilder().build();
    restored.putAll(entries);
  }
}

Prevention

When it happens

Trigger: A stream naming the unbounded cache view class directly instead of its SerializationProxy; serializers (Kryo/FST) that bypass writeReplace and instantiate the view; corrupted or version-mismatched streams.

Common situations: Persisting unbounded caches via Java serialization; custom serializers in grid/middleware products (Hazelstorm/Ignite custom interops) that reflectively build classes; cross-version cache migration.

Related errors


AI-assisted analysis of ben-manes/caffeine@9da6581ee3 (2026-08-14). Data as JSON: /api/errors/0a574f08a28fa085. Report an issue: GitHub.