bagisto/bagisto · error · AccessDeniedHttpException
Forbidden
Error message
Forbidden
What it means
Shop API AddressController::update loads the address with findOrFail($request->input('id')) — an unknown id 404s first — then compares $addressToUpdate->customer_id with the authenticated customer's id and aborts(403) on mismatch. It is an ownership guard: the address exists, but it belongs to a different customer than the session's.
Source
Thrown at packages/Webkul/Shop/src/Http/Controllers/API/AddressController.php:83
Event::dispatch('customer.addresses.create.after', $customerAddress);
return new JsonResource([
'data' => new AddressResource($customerAddress),
'message' => trans('shop::app.customers.account.addresses.index.create-success'),
]);
}
/**
* Update address for customer.
*/
public function update(AddressRequest $request): JsonResource
{
$customer = auth()->guard('customer')->user();
$addressToUpdate = $this->customerAddressRepository->findOrFail($request->input('id'));
if ($addressToUpdate->customer_id !== $customer->id) {
abort(403);
}
Event::dispatch('customer.addresses.update.before');
$customerAddress = $this->customerAddressRepository->update(array_merge($request->only([
'company_name',
'first_name',
'last_name',
'vat_id',
'address',
'country',
'state',
'city',
'postcode',
'phone',
'default_address',
'email',
]), [View on GitHub (pinned to 326bc45f17)
Solutions
- Refetch the authenticated customer's address list after login/switch and use only those ids.
- Clear persisted client state (localStorage address ids) on logout in SPAs.
- When merging accounts, re-map address ownership before calling update.
- Use the status difference for diagnosis: 404 = id unknown, 403 = id exists but is foreign.
Example fix
// before — stale id from a previous session
put('/api/address/update', { id: 42, ... }) // 42 belongs to another customer → 403
// after — take ids from the current customer's own list
const mine = await get('/api/customer/addresses');
await put('/api/address/update', { id: mine[0].id, ... }); Defensive patterns
Strategy: validation
Validate before calling
$address = $this->customerAddressRepository->find($request->input('id'));
$customerId = auth()->guard('customer')->id();
if (! $address || (int) $address->customer_id !== (int) $customerId) {
return response()->json(['message' => 'Address not available for this customer'], $address ? 403 : 404);
} Type guard
function ownsAddress(?object $address, int|string $customerId): bool
{
return $address !== null && (int) $address->customer_id === (int) $customerId;
} Try / catch
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
try {
$client->put('/api/address/update', $payload);
} catch (AccessDeniedHttpException $e) {
// address id belongs to another customer — refetch the session customer's own addresses
} Prevention
- Treat address ids as scoped per customer; never persist them across logins.
- Purge cached address ids in SPA state on logout.
- Write a test that cross-updates two customers' addresses and asserts the 403.
When it happens
Trigger: PUT/PATCH to the shop address-update API where the id belongs to another customer: stale id kept in SPA state after an account switch, request replayed from a different session, or automated clients reusing fixture ids across accounts.
Common situations: Customer logs into a different account in the same browser while the SPA keeps old address ids; QA scripts mixing fixtures from two customers; addresses re-parented after a guest-to-customer merge.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Not Found
- Not Found
- Forbidden
- admin::app.sales.refunds.create.invalid-qty
- admin::app.sales.refunds.create.invalid-qty
AI-assisted analysis of bagisto/bagisto@326bc45f17 (2026-08-17).
Data as JSON: /api/errors/2b2c1ba8b137bf41.
Report an issue: GitHub.