CacheHeaderParser.parseExpiresAt turns a Cache-Control: max-age=N into an absolute expiry using System.currentTimeMillis(). DocumentCache records when it cached a document using the Clock it was given, and compares the two — it only shortens its configured TTL when the server expiry is in the future.
Under Clock.systemUTC() those agree. Under an injected clock offset far from wall time they do not: every real server expiry reads as already past, so Cache-Control and Expires: are discarded wholesale and the configured interval governs alone.
That fallback is safe — nothing breaks, the document just refreshes on the SDK's own schedule — but it is silent, and it contradicts what the Clock constructors on JwksCache and MetadataCache advertise. Those are documented as supported API for driving refresh intervals deterministically, "from a simulation clock", which is exactly the usage that trips this.
The constructors now state the constraint. That is a workaround for a caller who reads the javadoc, not a fix.
Scope
Thread the clock into the parser: parseExpiresAt(Map<String, String> headers, Clock clock) as an additive overload, with the existing single-argument form delegating with Clock.systemUTC().
The awkward part is the path in between. Parsing happens in DocumentFetcher.from(HttpTransport), and DocumentFetcher is an interface whose two static factories — from and ssrfSafe — would need a clock parameter to pass one down. Both are public, so that is an API change and wants doing deliberately rather than as a drive-by. An alternative worth weighing first: have the fetcher return the raw directives and let DocumentCache compute the absolute expiry against its own clock, which puts the only clock read in the class that owns one.
Once either lands, the constraint paragraph in the Clock constructor javadocs comes back out.
CacheHeaderParser.parseExpiresAtturns aCache-Control: max-age=Ninto an absolute expiry usingSystem.currentTimeMillis().DocumentCacherecords when it cached a document using theClockit was given, and compares the two — it only shortens its configured TTL when the server expiry is in the future.Under
Clock.systemUTC()those agree. Under an injected clock offset far from wall time they do not: every real server expiry reads as already past, soCache-ControlandExpires:are discarded wholesale and the configured interval governs alone.That fallback is safe — nothing breaks, the document just refreshes on the SDK's own schedule — but it is silent, and it contradicts what the
Clockconstructors onJwksCacheandMetadataCacheadvertise. Those are documented as supported API for driving refresh intervals deterministically, "from a simulation clock", which is exactly the usage that trips this.The constructors now state the constraint. That is a workaround for a caller who reads the javadoc, not a fix.
Scope
Thread the clock into the parser:
parseExpiresAt(Map<String, String> headers, Clock clock)as an additive overload, with the existing single-argument form delegating withClock.systemUTC().The awkward part is the path in between. Parsing happens in
DocumentFetcher.from(HttpTransport), andDocumentFetcheris an interface whose two static factories —fromandssrfSafe— would need a clock parameter to pass one down. Both are public, so that is an API change and wants doing deliberately rather than as a drive-by. An alternative worth weighing first: have the fetcher return the raw directives and letDocumentCachecompute the absolute expiry against its own clock, which puts the only clock read in the class that owns one.Once either lands, the constraint paragraph in the
Clockconstructor javadocs comes back out.