DEV Community

onyxcross5743
onyxcross5743

Posted on

How to Debug Rejected DNS Record Writes When Zone IDs Meet Domain Names in Go

Short answer: treat a zone identifier and a domain name as two different types, even when an API returns both as strings. During an edtech registrar migration, resolve the authoritative zone from the fully qualified name, verify that the record belongs to that zone, and make the write idempotent. A fast cutover is useless if a retry writes to the wrong tenant or leaves DMARC records missing.

The least complex design is a small resolver-and-writer boundary in front of whichever standards-based DNS service you operate. It accepts a domain such as campus.example.edu, while the provider adapter alone handles an opaque zone ID. That boundary is where validation, audit fields, and retry policy belong.

Keep the types separate.

The trade-off is deliberate: validation rejects ambiguous input instead of guessing.

What is the error really telling you?

A rejection that says “zone id is not the domain name” usually means the request crossed two namespaces. The zone ID identifies an administrative object; the record name identifies a DNS owner name. They can both contain text, but equality is not a valid relationship. A zone called example.edu can have an opaque identifier such as z_7f31, and a record owner can be _dmarc.campus.example.edu.. Sending example.edu where z_7f31 is required, or sending z_7f31 into a domain-name field, should fail before the network call.

I initially treated this as a string-formatting defect. The more consequential bug was accepting a bare label (_dmarc) from a migration spreadsheet and silently attaching it to whichever zone the worker had cached. The fix was to make names absolute and to carry the resolved zone as a separate value all the way to the write operation.

How can a Go writer fix a rejected DNS record write because the zone ID is wrong?

Use typed inputs and a deterministic idempotency key. The adapter below is deliberately generic: LookupZone and ApplyRecord can wrap an internal service or a standards-compliant provider API.

package dnswrite

import (
    "context"
    "fmt"
    "strings"
)

type Zone struct { ID string; Name string }
type Record struct { Name string; Type string; Value string; TTL int }
type Client interface {
    LookupZone(context.Context, string) (Zone, error)
    ApplyRecord(context.Context, string, Record, string) error
}

func normalizeFQDN(s string) string {
    return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(s)), ".") + "."
}

func Write(ctx context.Context, c Client, domain string, r Record) error {
    fqdn := normalizeFQDN(domain)
    zone, err := c.LookupZone(ctx, fqdn)
    if err != nil { return fmt.Errorf("lookup zone for %q: %w", fqdn, err) }
    zoneName := normalizeFQDN(zone.Name)
    owner := normalizeFQDN(r.Name)
    if !strings.HasSuffix(owner, zoneName) { return fmt.Errorf("owner %q is outside zone %q", owner, zoneName) }
    key := fmt.Sprintf("dns:%s:%s:%s:%s", zone.ID, owner, r.Type, r.Value)
    if err := c.ApplyRecord(ctx, zone.ID, r, key); err != nil { return fmt.Errorf("apply record in zone %s: %w", zone.ID, err) }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The important line is not the suffix check by itself. It is the point at which the opaque zone.ID is selected once and then never reconstructed from user input. ApplyRecord should treat the key as an exactly-once intent: repeated delivery either returns the prior result or performs the same state transition. A database outbox with a unique key is a practical way to retain that property when the DNS API is not transactional.

Which propagation signal should gate an edtech cutover?

For a school platform, cutover speed and propagation delay pull in opposite directions. Lowering TTL shortly before a move can reduce the duration of stale answers, but recursive resolvers are allowed to retain data until the advertised TTL expires, and operational caches do not disappear on command. Therefore the gate should be authoritative, not merely “the API accepted my write.”

I use three observations: the desired record is present at the authoritative nameserver, two independent recursive resolvers return the expected value, and the migration ledger contains a matching audit event. If any observation disagrees, the worker keeps the old registrar path active for the planned overlap window. This is slower than a single success response and far faster than discovering during a class enrollment surge that half the domains still point at the old endpoint.

For a 2026 migration, I normally leave at least one advertised TTL between the final write and the cohort decision; that interval is a policy choice, not a protocol guarantee.

A compact audit row records tenant, normalized owner, type, previous value hash, new value hash, zone ID, request key, and timestamps for submitted, authoritative, and recursive confirmation. Keep the hashes and metadata longer than the DNS response itself; retaining every raw response forever creates storage and privacy obligations without improving reconciliation.

What does a safe validation test matrix contain?

Test the namespace boundary before testing propagation. Include a zone ID passed as a domain, a domain passed as a zone ID, a mixed-case owner, a trailing-dot variant, an owner outside the selected zone, and a retry after a simulated timeout. Add a DMARC record because its underscore label exposes normalization mistakes quickly, but do not assume DMARC syntax validates the zone relationship; RFC 7489 governs policy semantics, while zone ownership remains an infrastructure concern.

In deployment, ship the resolver and writer behind a feature flag, replay a sample of migration intents in dry-run mode, then promote by cohort rather than by calendar time. The decision rule is explicit: proceed when authoritative and recursive checks agree for the observation window; pause when they do not. A registrar-specific API can be removed only after the ledger shows that every pending intent has a terminal state and that rollback records are available.

This boundary is a poor fit for a one-off domain with no migration window: the audit ledger and recursive checks add latency and operational work. For that case, a manual change with a short, documented rollback can be the more honest choice. The stricter workflow earns its keep when many school tenants must move and reconciliation matters more than the first successful response.

The conclusion is intentionally unglamorous: preserve the distinction between administrative identity and DNS naming, and make propagation evidence part of the state machine. That discipline gives an edtech migration a controlled cutover without pretending that DNS is instantaneous.

Further reading

Top comments (0)