mirror of
https://github.com/alexjustesen/speedtest-tracker.git
synced 2026-06-23 04:10:25 +00:00
Fallback to http request when checking for internet connection (#2685)
Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com>
This commit is contained in:
@@ -6,22 +6,36 @@ use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Ping\Ping;
|
||||
use Spatie\Ping\PingResult;
|
||||
use Throwable;
|
||||
|
||||
class PingHostname
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(?string $hostname = null, int $count = 1): PingResult
|
||||
/**
|
||||
* Attempt to ping the given hostname. Returns null when the ping binary
|
||||
* is unavailable or another OS-level error prevents execution.
|
||||
*/
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
$hostname = $hostname ?? config('speedtest.preflight.internet_check_hostname');
|
||||
|
||||
// Remove protocol if present
|
||||
$hostname = preg_replace('#^https?://#', '', $hostname);
|
||||
|
||||
$ping = (new Ping(
|
||||
hostname: $hostname,
|
||||
count: $count,
|
||||
))->run();
|
||||
try {
|
||||
$ping = (new Ping(
|
||||
hostname: $hostname,
|
||||
count: $count,
|
||||
))->run();
|
||||
} catch (Throwable $e) {
|
||||
Log::debug('Ping command unavailable', [
|
||||
'host' => $hostname,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $ping->toArray();
|
||||
unset($data['raw_output'], $data['lines']);
|
||||
|
||||
@@ -11,6 +11,9 @@ use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class CheckForInternetConnectionJob implements ShouldQueue
|
||||
{
|
||||
@@ -46,11 +49,20 @@ class CheckForInternetConnectionJob implements ShouldQueue
|
||||
|
||||
$ping = PingHostname::run();
|
||||
|
||||
if ($ping->isSuccess()) {
|
||||
if ($ping?->isSuccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf('Failed to connected to hostname "%s". Error received "%s".', $ping->getHost(), $ping->error()?->value);
|
||||
Log::debug('Pinged failed, falling back to HTTP connectivity check');
|
||||
|
||||
// Ping either failed or was unavailable — attempt an HTTP fallback.
|
||||
if ($this->httpFallbackSucceeds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $ping === null
|
||||
? 'Ping command is unavailable and HTTP fallback also failed.'
|
||||
: sprintf('Failed to connected to hostname "%s". Error received "%s". HTTP fallback also failed.', $ping->getHost(), $ping->error()?->value);
|
||||
|
||||
$this->result->update([
|
||||
'data->type' => 'log',
|
||||
@@ -63,4 +75,39 @@ class CheckForInternetConnectionJob implements ShouldQueue
|
||||
|
||||
$this->batch()->cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to verify connectivity via an HTTP GET request as a fallback
|
||||
* when ping is unavailable or unsuccessful.
|
||||
*/
|
||||
protected function httpFallbackSucceeds(): bool
|
||||
{
|
||||
$url = config('speedtest.preflight.external_ip_url');
|
||||
|
||||
try {
|
||||
$response = Http::retry(3, 100)
|
||||
->timeout(5)
|
||||
->get(url: $url);
|
||||
|
||||
if ($response->ok()) {
|
||||
Log::debug('HTTP fallback connectivity check succeeded', ['url' => $url]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::debug('HTTP fallback connectivity check received non-OK response', [
|
||||
'url' => $url,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
} catch (Throwable $e) {
|
||||
Log::debug('HTTP fallback connectivity check failed', [
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\PingHostname;
|
||||
use App\Enums\ResultStatus;
|
||||
use App\Events\SpeedtestFailed;
|
||||
use App\Jobs\CheckForInternetConnectionJob;
|
||||
use App\Models\Result;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Spatie\Ping\PingResult;
|
||||
|
||||
beforeEach(function () {
|
||||
Event::fake();
|
||||
});
|
||||
|
||||
describe('CheckForInternetConnectionJob', function () {
|
||||
test('batch continues when ping succeeds', function () {
|
||||
$result = Result::factory()->create(['status' => ResultStatus::Started]);
|
||||
|
||||
$successfulPing = PingResult::fromArray(['success' => true, 'host' => 'icanhazip.com']);
|
||||
app()->bind(PingHostname::class, fn () => new class($successfulPing)
|
||||
{
|
||||
public function __construct(private PingResult $ping) {}
|
||||
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
return $this->ping;
|
||||
}
|
||||
});
|
||||
|
||||
[$job, $batch] = (new CheckForInternetConnectionJob($result))->withFakeBatch();
|
||||
$job->handle();
|
||||
|
||||
$this->assertFalse($batch->cancelled());
|
||||
$result->refresh();
|
||||
expect($result->status)->toBe(ResultStatus::Checking);
|
||||
Event::assertNotDispatched(SpeedtestFailed::class);
|
||||
});
|
||||
|
||||
test('batch continues when ping fails but HTTP fallback succeeds', function () {
|
||||
$result = Result::factory()->create(['status' => ResultStatus::Started]);
|
||||
|
||||
$failedPing = PingResult::fromArray([
|
||||
'success' => false,
|
||||
'error' => 'hostUnreachable',
|
||||
'host' => 'icanhazip.com',
|
||||
]);
|
||||
app()->bind(PingHostname::class, fn () => new class($failedPing)
|
||||
{
|
||||
public function __construct(private PingResult $ping) {}
|
||||
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
return $this->ping;
|
||||
}
|
||||
});
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response('1.2.3.4', 200),
|
||||
]);
|
||||
|
||||
[$job, $batch] = (new CheckForInternetConnectionJob($result))->withFakeBatch();
|
||||
$job->handle();
|
||||
|
||||
$this->assertFalse($batch->cancelled());
|
||||
$result->refresh();
|
||||
expect($result->status)->toBe(ResultStatus::Checking);
|
||||
Event::assertNotDispatched(SpeedtestFailed::class);
|
||||
});
|
||||
|
||||
test('batch continues when ping is unavailable but HTTP fallback succeeds', function () {
|
||||
$result = Result::factory()->create(['status' => ResultStatus::Started]);
|
||||
|
||||
app()->bind(PingHostname::class, fn () => new class
|
||||
{
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response('1.2.3.4', 200),
|
||||
]);
|
||||
|
||||
[$job, $batch] = (new CheckForInternetConnectionJob($result))->withFakeBatch();
|
||||
$job->handle();
|
||||
|
||||
$this->assertFalse($batch->cancelled());
|
||||
$result->refresh();
|
||||
expect($result->status)->toBe(ResultStatus::Checking);
|
||||
Event::assertNotDispatched(SpeedtestFailed::class);
|
||||
});
|
||||
|
||||
test('batch is cancelled when ping fails and HTTP fallback also fails', function () {
|
||||
$result = Result::factory()->create(['status' => ResultStatus::Started]);
|
||||
|
||||
$failedPing = PingResult::fromArray([
|
||||
'success' => false,
|
||||
'error' => 'hostUnreachable',
|
||||
'host' => 'icanhazip.com',
|
||||
]);
|
||||
app()->bind(PingHostname::class, fn () => new class($failedPing)
|
||||
{
|
||||
public function __construct(private PingResult $ping) {}
|
||||
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
return $this->ping;
|
||||
}
|
||||
});
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response('Service Unavailable', 503),
|
||||
]);
|
||||
|
||||
[$job, $batch] = (new CheckForInternetConnectionJob($result))->withFakeBatch();
|
||||
$job->handle();
|
||||
|
||||
$this->assertTrue($batch->cancelled());
|
||||
$result->refresh();
|
||||
expect($result->status)->toBe(ResultStatus::Failed);
|
||||
expect($result->data['level'])->toBe('error');
|
||||
expect($result->data['message'])->toContain('HTTP fallback also failed');
|
||||
Event::assertDispatched(SpeedtestFailed::class);
|
||||
});
|
||||
|
||||
test('batch is cancelled when ping is unavailable and HTTP fallback throws', function () {
|
||||
$result = Result::factory()->create(['status' => ResultStatus::Started]);
|
||||
|
||||
app()->bind(PingHostname::class, fn () => new class
|
||||
{
|
||||
public function handle(?string $hostname = null, int $count = 1): ?PingResult
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::failedConnection(),
|
||||
]);
|
||||
|
||||
[$job, $batch] = (new CheckForInternetConnectionJob($result))->withFakeBatch();
|
||||
$job->handle();
|
||||
|
||||
$this->assertTrue($batch->cancelled());
|
||||
$result->refresh();
|
||||
expect($result->status)->toBe(ResultStatus::Failed);
|
||||
expect($result->data['message'])->toBe('Ping command is unavailable and HTTP fallback also failed.');
|
||||
Event::assertDispatched(SpeedtestFailed::class);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user