fix(api): handle destination create races as conflicts

This commit is contained in:
Andras Bacsai 2026-07-02 15:33:01 +02:00
parent 4ef884e1ac
commit 70021c8d5e
2 changed files with 53 additions and 5 deletions

View file

@ -7,6 +7,7 @@
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -148,11 +149,19 @@ public function create(Request $request, string $server_uuid): JsonResponse
return response()->json(['message' => 'A destination with this network already exists on the server.'], 409);
}
$destination = $class::create([
'name' => $name,
'network' => $request->input('network'),
'server_id' => $server->id,
]);
try {
$destination = $class::create([
'name' => $name,
'network' => $request->input('network'),
'server_id' => $server->id,
]);
} catch (QueryException $exception) {
if ($this->isUniqueConstraintViolation($exception)) {
return response()->json(['message' => 'A destination with this network already exists on the server.'], 409);
}
throw $exception;
}
auditLog('api.destination.created', [
'team_id' => $teamId,
@ -165,6 +174,15 @@ public function create(Request $request, string $server_uuid): JsonResponse
return response()->json($this->transform($destination->load('server:id,uuid')), 201);
}
private function isUniqueConstraintViolation(QueryException $exception): bool
{
$sqlState = $exception->errorInfo[0] ?? null;
$driverCode = (string) ($exception->errorInfo[1] ?? $exception->getCode());
return in_array($sqlState, ['23000', '23505'], true)
|| in_array($driverCode, ['19', '1062', '2067'], true);
}
public function delete(Request $request, string $uuid): JsonResponse
{
$teamId = $this->teamIdOrAbort();

View file

@ -10,6 +10,7 @@
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
@ -193,6 +194,35 @@ function destinationsApiToken(User $user, Team $team, array $abilities): string
$response->assertStatus(409);
});
test('returns conflict when the database unique constraint wins a create race', function () {
$network = 'raced-network';
StandaloneDocker::creating(function (StandaloneDocker $destination) use ($network) {
if ($destination->network !== $network) {
return;
}
DB::table('standalone_dockers')->insert([
'name' => 'Concurrent destination',
'uuid' => (string) Str::uuid(),
'network' => $network,
'server_id' => $destination->server_id,
'created_at' => now(),
'updated_at' => now(),
]);
});
$response = $this->withHeaders(destinationsApiHeaders($this->bearerToken))
->postJson("/api/v1/servers/{$this->server->uuid}/destinations", [
'network' => $network,
]);
$response->assertStatus(409)
->assertJson(['message' => 'A destination with this network already exists on the server.']);
expect(StandaloneDocker::where('server_id', $this->server->id)->where('network', $network)->count())->toBe(1);
});
});
describe('DELETE /api/v1/destinations/{uuid}', function () {