nexu-io/open-design · error · DeployError
Cloudflare zone is required for a custom domain.
Error message
Cloudflare zone is required for a custom domain.
What it means
Thrown by normalizeCloudflarePagesDeploySelection in apps/daemon/src/deploy.ts:574 as `DeployError('Cloudflare zone is required for a custom domain.', 400)`. The function parses a custom-domain selection object; if all three of zoneId/zoneName/domainPrefix are empty it returns null (no custom domain requested), but if ANY field is present and `rawZoneId` is empty, it throws — a custom domain cannot be configured without the zone to create DNS records in.
Source
Thrown at apps/daemon/src/deploy.ts:574
};
}
function normalizeDeploymentLinkStatus(status: unknown): DeployLinkStatus {
return status === 'ready' || status === 'protected' || status === 'failed'
? status
: 'link-delayed';
}
function normalizeCloudflarePagesDeploySelection(input: unknown): CloudflarePagesDeploySelection | null {
if (!input || typeof input !== 'object') return null;
const source = input as JsonObject;
const rawZoneId = typeof source.zoneId === 'string' ? source.zoneId.trim() : '';
const rawZoneName = typeof source.zoneName === 'string' ? source.zoneName.trim() : '';
const rawPrefix = typeof source.domainPrefix === 'string' ? source.domainPrefix.trim() : '';
if (!rawZoneId && !rawZoneName && !rawPrefix) return null;
const zoneName = normalizeCloudflareZoneName(rawZoneName);
const domainPrefix = normalizeCloudflareDomainPrefix(rawPrefix);
if (!rawZoneId) throw new DeployError('Cloudflare zone is required for a custom domain.', 400);
if (!zoneName || !isValidCloudflareZoneName(zoneName)) {
throw new DeployError('Select a valid Cloudflare domain for the custom domain.', 400);
}
if (!domainPrefix) {
throw new DeployError('Enter a valid subdomain prefix, for example "demo".', 400);
}
return {
zoneId: rawZoneId,
zoneName,
domainPrefix,
hostname: `${domainPrefix}.${zoneName}`,
};
}
async function validateCloudflarePagesDeploySelection(config: DeployConfig, selection: CloudflarePagesDeploySelection | null): Promise<CloudflarePagesDeploySelection | null> {
if (!selection) return null;
const resp = await fetch(`${CLOUDFLARE_API}/zones/${encodeURIComponent(selection.zoneId)}`, {
headers: cloudflareHeaders(config),View on GitHub (pinned to 5be4028344)
Solutions
- Ensure the selection object includes a non-empty trimmed `zoneId` (the id from listCloudflarePagesZones).
- In the UI, require the zone dropdown to be selected before enabling the prefix field / submit.
- If only a domain name is known, resolve it to a zoneId first via listCloudflarePagesZones and match by zoneName.
Example fix
// before
normalizeCloudflarePagesDeploySelection({ zoneName: 'example.com', domainPrefix: 'demo' }); // no zoneId
// after
normalizeCloudflarePagesDeploySelection({ zoneId: 'abc', zoneName: 'example.com', domainPrefix: 'demo' }); Defensive patterns
Strategy: validation
Validate before calling
function hasZoneId(sel: unknown): boolean {
return typeof (sel as any)?.zoneId === 'string' && (sel as any).zoneId.trim().length > 0;
}
if (!hasZoneId(selection)) {
return res.status(400).json({ error: 'Select a Cloudflare zone.' });
} Type guard
function isCompleteZoneSelection(sel: unknown): sel is { zoneId: string; zoneName?: string; domainPrefix?: string } {
return typeof sel === 'object' && sel !== null
&& typeof (sel as any).zoneId === 'string'
&& (sel as any).zoneId.trim().length > 0;
} Try / catch
try {
await deployToCloudflarePages({ config, files, projectId, cloudflarePages: selection });
} catch (err) {
if (err instanceof DeployError && /zone is required/i.test(err.message)) {
return res.status(400).json({ error: 'Pick a zone before setting a custom domain.' });
}
throw err;
} Prevention
- Force the user to pick a zone from the dropdown (which carries zoneId) before enabling the prefix field.
- Send zoneId, zoneName, and domainPrefix together from a single selection object.
- Clear the whole selection if the zone dropdown is reset.
When it happens
Trigger: Submitting a custom-domain selection where `zoneName` or `domainPrefix` is set but `zoneId` is missing/empty after trimming. Commonly the frontend sent the human-readable domain and prefix from the form but the zone dropdown selection (which carries zoneId) was not included.
Common situations: Zone dropdown was cleared or never selected; stale form state where the user typed a prefix but the zone picker returned empty; frontend bug dropping the zoneId field from the payload.
Related errors
- Select a valid Cloudflare domain for the custom domain.
- Enter a valid subdomain prefix, for example "demo".
- cloudflare_zone_mismatch
- cloudflare_zone_inactive
- cloudflare_zone_not_full
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/b502eb85c9ff0b37.
Report an issue: GitHub.