[Laravel] Metadata cached forever from a missing table makes a resource permanently 500
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 62/100
調査の方向性
Start with the three cache decorators in src/Laravel/Metadata/ and compare their behavior with the guard in src/Laravel/Eloquent/Metadata/ModelMetadata.php. Reproduce the missing-table and table-recreated sequence in a non-debug Laravel setup, including a fresh worker. Done means degraded metadata is not persisted and a later worker resolves the resource correctly after the table returns.
索引モデルが issue の本文から書いたものです。
説明
API Platform version(s) affected: 4.4 (verified at 808c67198; the same code is on main)
Laravel, app.debug=false (production)
Description
If a Laravel worker boots while a resource's table is missing, API Platform caches the degraded metadata with rememberForever in a file-backed store. Every later request to that resource then returns 500 on the collection and 404 on the item. The state does not heal when the table comes back, it does not heal when the workers restart, and php artisan cache:clear very likely does not clear it.
The realistic trigger is a routine php artisan migrate during a deploy, or a container that starts before its migration job finishes.
Why it happens
ModelMetadata::getAttributes() already guards against this. It refuses to cache an empty result when the table is missing (src/Laravel/Eloquent/Metadata/ModelMetadata.php:106-110):
// Don't cache an empty result for a missing table: the table may be created later
if ([] === $result && !$schema->hasTable($table)) {
return $result;
}
The three decorators above it have no such guard. They cache whatever the inner factory returned, including the degraded value that ModelMetadata deliberately refused to keep:
src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php:37src/Laravel/Metadata/CachePropertyMetadataFactory.php:37src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php:35
return $this->localCache[$key] ??= Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $options) {
return $this->decorated->create($resourceClass, $options);
});
A missing table is silent: Schema::getColumns() on a dropped table returns [] and throws nothing, so nothing signals that the result is degraded.
Route registration reads this metadata at boot, not per request (src/Laravel/routes/api.php:36-38), so the poisoning happens on the first boot after the table goes missing, with no request needed.
Why it does not heal
The store is chosen by name and is file-backed in production (src/Laravel/ApiPlatformProvider.php:376 and :399):
true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file')
rememberForever on the file store writes to disk, so the value outlives the process. A new worker reads the same poisoned entry.
php artisan cache:clear takes an optional store argument. Without it, it flushes config('cache.default') only. API Platform does not use the default store, it uses api-platform.cache. If an application sets cache.default to redis or database and leaves api-platform.cache at file, the standard remediation does not touch the poisoned entry. Nothing aligns or documents those two settings.
Symptom
Once poisoned, against a resource whose table was missing at boot:
GET /api/<resources>returns 500, withUnable to generate an IRI for the item of type "..."raised fromsrc/Laravel/Routing/IriConverter.php:190.GET /api/<resources>/{id}returns 404.
How to reproduce
- Run a Laravel application with
app.debug=false, so the file store is selected. - Drop the table of an Eloquent-backed API resource.
- Boot a worker, for example by sending any request. Route registration resolves and caches the metadata.
- Recreate the table.
- Request that resource again, in a new process if you like. It still fails.
A direct call to ModelMetadata::getAttributes() recovers the correct attributes at step 5, which shows the inner guard works and the outer cache overrides it.
Clearing the decorators one at a time inside a running process does not recover the endpoint, because the routes were already built from the degraded metadata and Laravel's router is not rebuilt mid-process. Only clearing the correct store and starting a new process recovers it.
Suggested fix
Give the three decorators the same guard ModelMetadata already has: detect a degraded result and return it without caching, instead of protecting only the innermost layer.
This does not make the failure disappear. The boot that happens while the table is missing still registers wrong routes. It does make the failure transient: the next worker start after the migration finishes resolves the metadata correctly. That is the difference between a deploy hiccup and an outage that persists until somebody finds the right store to clear.
A finite TTL instead of rememberForever would also bound the damage, and is smaller to implement, but it self-heals only after the interval and changes cache-hit behavior.
Notes
- This is Laravel-specific in practice. The Symfony bridge caches without a TTL too (
src/Metadata/Property/Factory/CachedPropertyNameCollectionFactory.phpand siblings), but Symfony property discovery comes from Doctrine mapping, attributes and reflection, which do not race against migrations the way Eloquent's runtime schema introspection does. - Not verified: whether
php artisan route:cache, common in production, makes recovery harder still by keeping the bad routes across restarts untilroute:clearruns.
- 主要言語
- PHP
- スター
- 2.6k
- フォーク
- 987
- 平均マージ
- 1日 8時間
- マージ済み PR(30日)
- 80
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
api-platform/core のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
api-platform/core#8665 ·
メンテナーはふだん 1 日以内に返信
-
Doctrine\Orm\OrderExtension fails on SortDirection対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
api-platform/core#8660 ·
メンテナーはふだん 1 日以内に返信
-
DeserializeProvider calls PartialDenormalizationException::getErrors(), deprecated in Symfony 8.1対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
api-platform/core#8650 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
api-platform/core#8649 ·
メンテナーはふだん 1 日以内に返信
-
`OrderExtension` and `OrderFilter` pass string sort directions, deprecated since `doctrine/orm` 3.7オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
api-platform/core#8648 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
api-platform/core の issue をすべて見る
似ている issue
-
FrameworkBundle SecurityBundle
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
symfony/symfony-docs#23277 ·
メンテナーはふだん 3 日以内に返信
-
[Sync EN] hard link does not need special permissions対応中かも @lacatoire が今日担当しました。 オープンsync-en
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
メンテナーはふだん 1 日以内に返信
-
sync-en
難易度 1/5 1時間未満 初心者へのやさしさ 76/100
メンテナーはふだん 4 日以内に返信
-
feature-request
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
omegaup/omegaup#10323 · コメント 1 件 ·
メンテナーはふだん 5 日以内に返信
-
bug mod:appointments
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
3pontos-tech/gil-benefits#303 ·