In a Laravel API, it’s really common to create users with an endpoint like this in a user
controller:
public function store(Request $request): UserResource|JsonResponse
{
$validator = Validator::make(
$request->all(),
[
'email' => 'required|string|max:255|email|unique:users',
'name' => 'required|string|max:255',
],
[
'email.unique' => 'That email address already has an account.',
]
);
if ($validator->fails()) {
return response()->json(
[
'error' => true,
'message' => $validator->errors()->all(),
],
Response::HTTP_UNPROCESSABLE_ENTITY
);
}
$user = User::create(
$request->only(
[
'email',
'name',
]
)
);
Code language: PHP (php)
There’s a problem here though – that unique
validation on the email field is subject to a race condition. If two requests are received very close together, both can pass validation, but then the second one will fail with a duplicate key error on the User::create
call. While that sounds unlikely, it happens for real sometimes, and you’ll see something like this in your web logs when it does:
192.168.0.1 - - [06/Oct/2023:07:08:54 +0000] "POST /users/ HTTP/2.0" 201 1276 "-" "okhttp/4.9.2"
192.168.0.1 - - [06/Oct/2023:07:08:55 +0000] "POST /users/ HTTP/2.0" 500 17841 "-" "okhttp/4.9.2"
Code language: JavaScript (javascript)
The 201
response is a successful creation, but it’s followed a second later by a 500
failure for the duplicate request. The Laravel log will then contain one of these:
[2023-10-06 07:08:55] staging.ERROR: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'user@example.com'
for key 'users.users_email_unique' (Connection: mysql, SQL: insert into `users` (`email`, `name`) values (user@example.com, Test)
Code language: JavaScript (javascript)
To deal with that we can trap the creation error, and return an error response that looks the same as the validation error:
try {
$user = User::create(
$request->only(
[
'email',
'name',
]
)
);
} catch (QueryException $e) {
//1062 is the MySQL code for duplicate key
if ($e->errorInfo[1] !== 1062) {
//Rethrow anything except a duplicate key error
throw $e;
}
return response()->json(
[
'error' => true,
'message' => 'That email address already has an account.',
],
Response::HTTP_UNPROCESSABLE_ENTITY
);
}
Code language: PHP (php)
This way, as far as the client is concerned, it was a straightforward validation failure with an appropriate 422
error code, and we don’t get spurious 500s clogging up our error logs.