Return graceful output from get_stats instead of a 500

get_stats/{id} fataled when the item id was missing and 500'd whenever an
enhanced app's livestats() threw - a broken or updated remote app definition
(e.g. Komga) took the whole request down, and the frontend then stopped
refreshing that tile entirely.

getStats now returns valid JSON (200) with an inactive/empty payload when the
item is missing, has no class, references a stale class, or throws, logging
the failure for diagnosis. The successful path is unchanged and returns the
livestats output verbatim.

Resolves #1558
This commit is contained in:
KodeStar
2026-07-08 15:21:39 +01:00
parent 5907a1f231
commit f547ae42bb
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>']);
}
}