diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index fae18efab..9c979e00a 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -71,7 +71,7 @@ class Handler extends ExceptionHandler 'message' => $exception->getMessage(), 'errors' => $exception->validator->getMessageBag(), ], - method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500 + method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : $exception->status ); } elseif ($request->wantsJson()) { return response()->json( diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index f9878281a..e85afec64 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2607,8 +2607,8 @@ class ApiV1Controller extends Controller $this->validate($request, [ 'page' => 'sometimes|integer|max:40', - 'min_id' => 'sometimes|integer|min:0|max:'.PHP_INT_MAX, - 'max_id' => 'sometimes|integer|min:0|max:'.PHP_INT_MAX, + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, 'limit' => 'sometimes|integer|min:1', 'include_reblogs' => 'sometimes', ]); diff --git a/tests/Feature/HomeTimelineTest.php b/tests/Feature/HomeTimelineTest.php new file mode 100644 index 000000000..34ac3a015 --- /dev/null +++ b/tests/Feature/HomeTimelineTest.php @@ -0,0 +1,106 @@ +create(); + Passport::actingAs($user, ['read']); + + return $user; + } + + // ------------------------------------------------------------------------- + // Bug #6609 — Exception handler returns 500 instead of 422 + // ------------------------------------------------------------------------- + + /** + * ValidationException must produce HTTP 422, not 500. + * + * Root cause: Handler.php called method_exists($exception, 'getStatusCode') + * which is false for Illuminate\Validation\ValidationException (the class + * exposes $status = 422 but has no getStatusCode() method), so the fallback + * of 500 was used for every JSON API validation failure. + */ + #[Test] + public function json_api_validation_failure_returns_422_not_500(): void + { + $this->authenticatedUser(); + + // max_id=not-a-number is non-null and not an integer. + // 'nullable|integer' fails on it → ValidationException → must be 422. + $response = $this->getJson('/api/v1/timelines/home?max_id=not-a-number'); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + $response->assertJsonPath('errors.max_id.0', fn ($msg) => str_contains($msg, 'integer')); + } + + // ------------------------------------------------------------------------- + // Bug #6610 — timelineHome rejects empty max_id= / min_id= + // ------------------------------------------------------------------------- + + /** + * An empty max_id= query parameter must not cause a server error. + * + * Root cause: timelineHome used 'sometimes|integer' for max_id/min_id while + * every other timeline method in ApiV1Controller used 'nullable|integer'. + * The ConvertEmptyStringsToNull middleware converts max_id= to null before + * validation. With 'sometimes', the key is still "present" (as null) and the + * integer rule runs on null → ValidationException → triggered bug #6609 → 500. + * With 'nullable', a null value skips the integer check → no exception. + * + * The Pixelfed Android app (okhttp/4.12.0) sends max_id= on every first + * home timeline load, making the app unusable without this fix. + */ + #[Test] + public function timeline_home_accepts_empty_max_id_parameter(): void + { + $this->authenticatedUser(); + + // Mirrors the exact request sent by the Pixelfed Android app on first load. + $response = $this->getJson('/api/v1/timelines/home?limit=20&max_id=&_pe=1&limit=20'); + + $response->assertSuccessful(); + } + + #[Test] + public function timeline_home_accepts_empty_min_id_parameter(): void + { + $this->authenticatedUser(); + + $response = $this->getJson('/api/v1/timelines/home?limit=20&min_id='); + + $response->assertSuccessful(); + } + + #[Test] + public function timeline_home_still_validates_non_null_non_integer_max_id(): void + { + // Ensure the nullable change does not make the endpoint accept garbage input. + // A non-empty, non-integer max_id must still fail validation (422, not 200). + $this->authenticatedUser(); + + $response = $this->getJson('/api/v1/timelines/home?max_id=abc'); + + $response->assertStatus(422); + } +}