Skip to content

fix(symfony,laravel): IriConverter local cache key collision between item and collection ops#7975

Open
soyuka wants to merge 2 commits into
api-platform:4.3from
soyuka:fix-iri-converter-cache-key-collision
Open

fix(symfony,laravel): IriConverter local cache key collision between item and collection ops#7975
soyuka wants to merge 2 commits into
api-platform:4.3from
soyuka:fix-iri-converter-cache-key-collision

Conversation

@soyuka
Copy link
Copy Markdown
Member

@soyuka soyuka commented May 15, 2026

Summary

The IriConverter::$localOperationCache key in both the Symfony and Laravel routing converters used a suffix scheme that collided for legitimately distinct call patterns. Under worker runtimes (FrankenPHP, Swoole, Octane) the converter survives across requests, so the collision corrupts IRI generation for any subsequent request that lands on the leaked slot.

Symfony (src/Symfony/Routing/IriConverter.php): _c was shared by is_string(\$resource) AND \$operation instanceof CollectionOperationInterface. After a POST that triggers PurgeHttpCacheListener::postFlush, a GetCollection operation is cached under '' . Class . '_c'. Any later call like getIriFromResource(Class::class, ABS_PATH, new Get(), ['uri_variables' => ['id' => 1]]) hits the same slot and gets the cached GetCollection — producing /things?id=1 instead of /things/1. AbstractItemNormalizer::getResourceFromIri then throws Invalid IRI.

Laravel (src/Laravel/Routing/IriConverter.php): broader scope — the key didn't check CollectionOperationInterface at all, so any string-resource call shared one slot regardless of operation type. PurgeHttpCacheListener already primes that slot with a GetCollection on every save/delete (see src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php:50).

Fix

Both converters now encode the two axes independently in the cache suffix:

  • _s / _o — string vs object resource
  • _c / _i — collection vs item operation

Yielding four distinct slots (_s_c, _s_i, _o_c, _o_i) with no overlap.

Test plan

  • tests/Functional/Json/RelationTest::testCreateRelatedDummyWithPlainIdentifierForRelation — end-to-end Symfony repro using RelatedDummyPlainIdentifierDenormalizer; sequential POST /third_levels then POST /related_dummies in the same kernel (\$alwaysBootKernel = false). Fails on 4.3 without the fix with Invalid IRI "/third_levels?id=1".
  • src/Laravel/Tests/Unit/Routing/IriConverterTest::testLocalCacheKeyDistinguishesItemAndCollectionForStringResource — unit-level repro: anonymous new GetCollection() then anonymous new Get() for the same class-string. Fails on 4.3 without the fix (returns /api/books instead of /api/books/1).
  • tests/Symfony/Routing/IriConverterTest (14 tests) and the broader tests/Symfony/Routing/ + tests/Functional/ run (691 tests) stay green.
  • Laravel PurgerTest failures pre-exist on 4.3 and are unrelated to this change (verified by stashing the fix).

soyuka added 2 commits May 14, 2026 08:30
…item and collection ops

The `_c` suffix was shared by string-resource and collection-op calls, so two unrelated
callsites with the same operation name and resource class returned each other's cached
operation. Surfaces under worker runtimes (FrankenPHP, Swoole) where the converter
persists across requests.

Encode both axes independently: `_s|_o` for string vs object resource, `_c|_i` for
collection vs item operation.
…item and collection ops

Same collision as the Symfony converter — actually broader here, since the Laravel
key didn't check `CollectionOperationInterface` at all: any string-resource call
shared one slot regardless of operation type. Surfaces under Octane/worker mode
where the converter persists across requests.

Encode both axes independently: `_s|_o` for string vs object resource, `_c|_i` for
collection vs item operation.
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