Hydration mismatch pada dashboard audit DNS terjadi ketika pohon DOM yang dihasilkan server (SSR) berbeda dengan pohon DOM yang dibuat browser saat hidrasi. Pada evaluasi record DMARC modern, ketidakkonsistenan ini sering dipicu oleh pengujian tag np (Non-existent Subdomain Policy) dan validasi cryptographic proof DNSSEC (NSEC/NSEC3).

Akar Masalah: Divergensi State DNSSEC Resolver SSR dan Client DoH

Evaluasi tag DMARC np bergantung langsung pada pembuktian ketiadaan subdomain (authenticated denial of existence). Backend SSR mengevaluasi NSEC/NSEC3 menggunakan recursive resolver internal (seperti BIND atau Unbound) yang memvalidasi bendera Authentic Data (AD). Resolver ini memiliki trust anchor lokal dan cache record tersendiri.

Masalah muncul ketika komponen frontend mencoba melakukan re-evaluasi langsung pada fase inisialisasi menggunakan DNS-over-HTTPS (DoH) pihak ketiga (misalnya Cloudflare 1.1.1.1 atau Google 8.8.8.8). Perbedaan ini memicu divergensi:

  • AD Flag Divergence: Resolver internal menandai NSEC3 valid (AD=1), sedangkan DoH publik mungkin memblokir query karena rate limiting atau format OPT RR EDNS yang berbeda (AD=0).
  • Propagation Lag: Respon NSEC3 NXDOMAIN dari resolver backend sudah diperbarui, namun cache resolver DoH edge masih menyimpan data lama (stale).
  • Komparasi Tag np: Node tree merender badge <span class="badge-secure">Valid NP: Reject</span> di server, tetapi browser mengeksekusi fetch DoH sebelum hidrasi selesai dan merender <span class="badge-warning">NSEC Unverified</span>.

Anti-Pattern: Re-evaluasi Asinkron pada Fase Render

Kesalahan umum adalah membaca status DNS langsung di dalam render pass komponen tanpa menjamin determinisme serialisasi data SSR.

// ANTI-PATTERN: Memicu Hydration Mismatch
export default function DmarcChecker({ domain, initialReport }) {
  const [report, setReport] = useState(initialReport);

  // BUG: Resolver DoH di browser mengevaluasi ulang seketika saat render pertama
  if (typeof window !== 'undefined' && !report.dnssecValid) {
    fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`)
      .then(res => res.json())
      .then(dohData => {
        // Menyebabkan DOM tree browser tidak identik dengan markup SSR
        setReport(parseDoh(dohData));
      });
  }

  return (
    <div>
      {report.hasNpTag ? (
        <span className={report.dnssecValid ? "valid" : "invalid"}>
          NP: {report.npPolicy}
        </span>
      ) : (
        <span>NP Defaulted to P</span>
      )}
    </div>
  );
}

Solusi: Server Boundary dan Progressive Client Polling

Kunci eliminasi hydration mismatch adalah arsitektur unidirectional deterministic payload:

  1. Server Isolation: Seluruh parsing tag DMARC (RFC 7489/RFC 9091) dan validasi cryptographic proof NSEC/NSEC3 harus diselesaikan di sisi server secara tuntas sebelum rendering HTML.
  2. Deterministic Hydration: Serialisasikan hasil evaluasi ke dalam initial state props. Markup HTML hasil render server harus identik dengan inisialisasi state pertama di browser.
  3. Progressive Revalidation: Lakukan query berkala atau manual refresh hanya di dalam lifecycle effect (useEffect) setelah proses hidrasi komponen selesai secara utuh.

Implementasi Komponen Deterministik

Berikut refaktor kode menggunakan Next.js / React dengan contract data yang terisolasi:

// Tipe Payload Deterministik
interface DmarcEvaluation {
  domain: string;
  hasNpTag: boolean;
  npPolicy: 'reject' | 'quarantine' | 'none' | 'unset';
  dnssecStatus: 'SECURE' | 'INSECURE' | 'BOGUS';
  authenticatedDenial: boolean; // NSEC/NSEC3 validation result
}

// IMPLEMENTASI BENAR: Server Boundary + Safe Hydration
import { useState, useEffect } from 'react';

export default function SafeDmarcChecker({ initialPayload }: { initialPayload: DmarcEvaluation }) {
  // 1. Inisialisasi state murni dari server payload (Zero-diff DOM)
  const [data, setData] = useState<DmarcEvaluation>(initialPayload);
  const [isRefreshing, setIsRefreshing] = useState(false);

  // ponytail: polling otomatis di-skip; tambahkan interval jika dashboard butuh real-time NSEC watcher.
  const refreshCheck = async () => {
    setIsRefreshing(true);
    try {
      const res = await fetch(`/api/dns/verify?domain=${encodeURIComponent(data.domain)}`);
      const updated: DmarcEvaluation = await res.json();
      setData(updated);
    } finally {
      setIsRefreshing(false);
    }
  };

  return (
    <div className="audit-card">
      <div className="status-row">
        <span>Kebijakan Subdomain Non-Existent (np):</span>
        <span className={`badge ${data.authenticatedDenial ? 'badge-verified' : 'badge-unverified'}`}>
          {data.hasNpTag ? `np=${data.npPolicy}` : 'Tag np tidak dikonfigurasi'}
        </span>
      </div>
      <div className="status-row">
        <span>Integritas DNSSEC (NSEC/NSEC3):</span>
        <span className={`badge badge-${data.dnssecStatus.toLowerCase()}`}>
          {data.dnssecStatus}
        </span>
      </div>
      <button onClick={refreshCheck} disabled={isRefreshing}>
        {isRefreshing ? 'Memverifikasi...' : 'Re-evaluasi Resolver'}
      </button>
    </div>
  );
}

Verifikasi dan Debugging

Pastikan backend API resolver meneruskan respons DNSSEC lengkap beserta bit status. Periksa keabsahan payload via curl sebelum markup dikirim ke client:

# Verifikasi integritas DNSSEC AD flag dari server backend
dig @127.0.0.1 _dmarc.example.com TXT +dnssec +multiline

Cari flag ad; di header respons. Jika flag ad tidak ditemukan, jangan tandai field authenticatedDenial bernilai true pada SSR props. Konsistensi nilai boolean ini di level server mencegah terjadinya perbedaan tree HTML pada siklus hidrasi client.