---
id: w7/m6/t003
title: Surface the typed rejection across REST · GraphQL · MCP + the dashboard add-domain flow
worker: worker7
status: todo
estimate: 30m
depends_on: [w7/m6/t001, w7/m6/t002]
---

## Objective

Make the collision and reserved-host rejections (t001/t002) render as a clear validation error on every surface that adds a custom domain — REST, GraphQL, MCP, and the dashboard — with a consistent, Render-shaped message ("this domain is already in use" / "this host is reserved"), not a 500 or an opaque error.

## Context

- REST: `lego/backend/internal/apps/rest.go` — `POST …/custom-domains` maps `core.ErrBadRequest`/conflict to the right status (400/409) + message.
- GraphQL: the `addCustomDomain` resolver — surface the typed error as a GraphQL error the dashboard can display.
- MCP: the add-custom-domain tool — return the rejection as a tool error, not a crash.
- Dashboard: the add-domain form (the DNS-instructions / custom-domains panel, w5/m10) — show the validation message inline.
- Render's wording for reference: "This domain already exists on another site. Please delete it from that site and try again."

## Steps

1. Ensure the typed error from t001/t002 maps to a 400/409 with a stable message on REST, and to an equivalent error on GraphQL + MCP.
2. Dashboard: catch the rejection on add and render it inline on the form (do not leave the user with a generic failure).
3. Keep the message wording consistent across the three adapters.

## Files

- `lego/backend/internal/apps/rest.go`
- GraphQL `addCustomDomain` resolver
- MCP add-custom-domain tool
- `dashboard/` — custom-domain add form

## Acceptance criteria

- [ ] A collision or reserved-host add returns a 400/409 with a consistent message on REST, and an equivalent typed error on GraphQL + MCP.
- [ ] The dashboard shows the rejection inline on the add-domain form.

## Out of scope

- The guard logic itself (t001/t002); the parity audit vs Render (t004).
