HeyPuter/puter · error · HttpError
forbidden
forbidden
Error message
Cannot delete a protected subdomain
What it means
The site row exists and is owned by the caller, but its `protected` flag is set, so the controller refuses deletion with HTTP 403. Protected subdomains are reserved system sites that must not be removed through the public delete endpoint. This is a deliberate guard, not a bug.
Source
Thrown at src/backend/controllers/hosting/HostingController.js:85
const { site_uuid } = req.body ?? {};
if (!site_uuid || typeof site_uuid !== 'string') {
throw new HttpError(400, 'Missing or invalid `site_uuid`', {
legacyCode: 'bad_request',
});
}
const row = await this.subdomainStore.getByUuid(site_uuid, {
userId: req.actor.user.id,
});
if (!row) {
throw new HttpError(
404,
'Site not found or not owned by you',
{ legacyCode: 'not_found' },
);
}
if (row.protected) {
throw new HttpError(
403,
'Cannot delete a protected subdomain',
{ legacyCode: 'forbidden' },
);
}
await this.subdomainStore.deleteByUuid(site_uuid, {
userId: req.actor.user.id,
});
res.json({});
},
);
}
onServerStart() {}
onServerPrepareShutdown() {}
onServerShutdown() {}View on GitHub (pinned to 908ec23eda)
Solutions
- Do not attempt to delete protected subdomains via the public API — they are intentionally locked.
- If protection is no longer needed, unprotect the site through the admin/DB layer first, then retry.
- Filter protected sites out of any bulk-delete or cleanup script.
- Surface a clear message to the user that this site cannot be removed.
Example fix
// before — delete without checking
await api.call('delete-site', { site_uuid: site.uuid });
// after — skip protected sites in bulk operations
if (site.protected) {
console.log(`Skipping protected site: ${site.subdomain}`);
continue;
}
await api.call('delete-site', { site_uuid: site.uuid }); Defensive patterns
Strategy: validation
Validate before calling
// Check the protected flag from the site list before attempting delete
const sites = await api.call('hosting/sites');
const site = sites.find(s => s.uuid === site_uuid);
if (site?.protected) {
alert('This site is protected and cannot be deleted.');
return;
}
await api.call('delete-site', { site_uuid }); Type guard
/** @typedef {{ uuid: string, protected?: boolean }} SiteRow */
/** @param {SiteRow} s @returns {boolean} */
function isDeletable(s) {
return !s.protected;
} Try / catch
try {
await api.call('delete-site', { site_uuid });
} catch (e) {
if (e.code === 'forbidden') {
console.error('This site is protected and cannot be deleted.');
} else throw e;
} Prevention
- Fetch the `protected` flag from the site list and disable/hide delete buttons for protected sites.
- Filter protected sites out of bulk-delete scripts.
- Communicate to users why certain sites cannot be removed.
When it happens
Trigger: Calling `POST /delete-site` with the UUID of a protected subdomain (e.g., a default/system-hosted site). The store returns the row, but `row.protected` is truthy.
Common situations: Trying to clean up a default site created during provisioning; an admin marked a site protected and a user attempts deletion; automated cleanup scripts that iterate all sites without checking the flag.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/2739c0984503ef54.
Report an issue: GitHub.