TechnitiumSoftware/DnsServer · error · InvalidOperationException

Cannot update record: use SetRecords() for

Error message

Cannot update record: use SetRecords() for {0} record

What it means

Thrown by ForwarderZone.UpdateRecord() when oldRecord.Type is SOA. SOA records are special single-valued apex records whose semantics (serial, refresh, retry, expire, minimum, name server, responsible person) cannot be patched via the generic add/delete UpdateRecord flow; they must be atomically replaced via SetRecords(SOA, ...), which validates TTL<=Expire, Retry<=Refresh, Refresh<=Expire, and rewrites the responsible person to 'invalid'. The message echoes the offending type so the caller knows exactly which record type to redirect.

Solutions

  1. Route SOA edits through SetRecords(DnsResourceRecordType.SOA, new[] { newSoaRecord }) instead of UpdateRecord.
  2. Branch in your API/UI layer: if (oldRecord.Type == DnsResourceRecordType.SOA) zone.SetRecords(oldRecord.Type, new[] { newRecord }); else zone.UpdateRecord(oldRecord, newRecord);
  3. Ensure the new SOA passes SetRecords validation (TTL <= Expire, Retry <= Refresh, Refresh <= Expire).

Example fix

// before
zone.UpdateRecord(oldRecord, newRecord);

// after
if (oldRecord.Type == DnsResourceRecordType.SOA)
    zone.SetRecords(DnsResourceRecordType.SOA, new[] { newRecord });
else
    zone.UpdateRecord(oldRecord, newRecord);
Defensive patterns

Strategy: validation

Validate before calling

if (oldRecord.Type == DnsResourceRecordType.SOA)
    zone.SetRecords(DnsResourceRecordType.SOA, new[] { newRecord });
else
    zone.UpdateRecord(oldRecord, newRecord);

Type guard

static bool RequiresSetRecordsInsteadOfUpdate(DnsResourceRecordType type) => type == DnsResourceRecordType.SOA;

Try / catch

try { zone.UpdateRecord(oldRecord, newRecord); }
catch (InvalidOperationException ex) when (oldRecord.Type == DnsResourceRecordType.SOA) { zone.SetRecords(DnsResourceRecordType.SOA, new[] { newRecord }); }

Prevention

When it happens

Trigger: Calling zone.UpdateRecord(oldSoaRecord, newSoaRecord) where oldRecord.Type == DnsResourceRecordType.SOA on a ForwarderZone. Happens when generic record-edit UI/API code passes an SOA row into the same update handler used for A/AAAA/CNAME records.

Common situations: A generic 'edit record' API endpoint that does not branch on record type; an import/sync routine that round-trips SOA through the update path; clients editing SOA timers (refresh/retry/expire) via the wrong endpoint.

Related errors


AI-assisted analysis of TechnitiumSoftware/DnsServer@d0484b6c1e (2026-08-13). Data as JSON: /api/errors/a7fa9d5f6b5535a9. Report an issue: GitHub.

Appendix: source

Thrown at DnsServerCore/Dns/Zones/ForwarderZone.cs:243

                    if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord))
                    {
                        CommitAndIncrementSerial([deletedRecord]);

                        TriggerNotify();

                        return true;
                    }

                    return false;
            }
        }

        public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)
        {
            switch (oldRecord.Type)
            {
                case DnsResourceRecordType.SOA:
                    throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record");

                default:
                    if (oldRecord.Type != newRecord.Type)
                        throw new InvalidOperationException("Old and new record types do not match.");

                    if (newRecord.OriginalTtlValue > GetZoneSoaExpire())
                        throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE.");

                    if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord))
                        throw new DnsServerException("Cannot update record: the record does not exists to be updated.");

                    AddRecord(newRecord, out IReadOnlyList<DnsResourceRecord> addedRecords, out IReadOnlyList<DnsResourceRecord> deletedRecords);

                    List<DnsResourceRecord> allDeletedRecords = new List<DnsResourceRecord>(deletedRecords.Count + 1);
                    allDeletedRecords.Add(deletedRecord);
                    allDeletedRecords.AddRange(deletedRecords);

                    CommitAndIncrementSerial(allDeletedRecords, addedRecords);

View on GitHub (pinned to d0484b6c1e)