Merge pull request #1570 from linuxserver/fix/getstats-graceful-failure

Return graceful output from get_stats instead of a 500
This commit is contained in:
KodeStar
2026-07-08 18:28:55 +01:00
committed by GitHub
2 changed files with 121 additions and 6 deletions
+34 -6
View File
@@ -587,18 +587,46 @@ class ItemController extends Controller
}
/**
* @param $id
* @return void
* Return live stats for an enhanced application tile.
*
* Always responds with HTTP 200 and valid JSON so the frontend refresh
* loop (liveStatRefresh.js) keeps re-queueing the tile. On any failure we
* degrade gracefully to an inactive, empty tile instead of a 500.
*
* @param int|string $id
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
*/
public function getStats($id)
{
$item = Item::find($id);
$graceful = response()->json(['status' => 'inactive', 'html' => '']);
$item = Item::find($id);
if ($item === null) {
return $graceful;
}
// Non-enhanced items (or stale records) have no live-stats class.
if (empty($item->class)) {
return $graceful;
}
try {
$config = $item->getconfig();
// Guard against a stale/renamed class string from the remote apps repo.
if (! class_exists($item->class)) {
return $graceful;
}
$config = $item->getconfig();
if (isset($item->class)) {
$application = new $item->class;
$application->config = $config;
echo $application->livestats();
// livestats() returns a JSON string; return it verbatim (no re-encoding).
return response($application->livestats());
} catch (\Throwable $e) {
Log::error('getStats failed for item '.$id.' ('.$item->class.'): '.$e->getMessage());
return $graceful;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace Tests\Feature;
use App\Item;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Fixture app whose live stats rendering throws, mirroring the Komga
* failure from issue #1558 (broken remote blade / upstream API error).
*/
class ThrowingStatApp
{
public $config;
public function livestats()
{
throw new \Exception('boom');
}
}
/**
* Fixture app whose live stats rendering succeeds and returns the JSON
* string the frontend expects.
*/
class HappyStatApp
{
public $config;
public function livestats()
{
return json_encode(['status' => 'active', 'html' => '<b>ok</b>']);
}
}
class GetStatsTest extends TestCase
{
use RefreshDatabase;
public function test_missing_item_id_does_not_500(): void
{
$response = $this->get('get_stats/999999');
$response->assertStatus(200);
$response->assertJson(['status' => 'inactive', 'html' => '']);
}
public function test_throwing_app_degrades_gracefully(): void
{
$item = Item::factory()->create([
'class' => ThrowingStatApp::class,
]);
$response = $this->get('get_stats/'.$item->id);
$response->assertStatus(200);
$response->assertJson(['status' => 'inactive', 'html' => '']);
}
public function test_item_with_no_class_degrades_gracefully(): void
{
$item = Item::factory()->create([
'class' => null,
]);
$response = $this->get('get_stats/'.$item->id);
$response->assertStatus(200);
$response->assertJson(['status' => 'inactive', 'html' => '']);
}
public function test_happy_path_returns_livestats_output_verbatim(): void
{
$item = Item::factory()->create([
'class' => HappyStatApp::class,
]);
$expected = json_encode(['status' => 'active', 'html' => '<b>ok</b>']);
$response = $this->get('get_stats/'.$item->id);
$response->assertStatus(200);
$this->assertSame($expected, $response->getContent());
$response->assertJson(['status' => 'active', 'html' => '<b>ok</b>']);
}
}