louis-e/arnis · error · Error

Unexpected preview payload format

Error message

Unexpected preview payload format

What it means

readPayloadHeader normalizes a Tauri invoke result into an ArrayBuffer and checks the 4-byte magic prefix against the expected value ('APV1' for DEM payloads, 'APL1' for land cover). If the first four bytes do not match, the payload is not the expected binary format — it may be an error blob, a different version, or garbage — so this Error is thrown before any DataView field parsing happens.

Source

Thrown at src/gui/js/preview3d.js:68

  let generationRunning = false;
  let protocolRegistered = false;

  // ---------- payload parsing and DEM tile synthesis ----------

  // Normalizes a Tauri invoke result to an aligned ArrayBuffer and validates
  // the payload magic; every payload layout starts with magic + grid dims.
  function readPayloadHeader(buffer, expectedMagic) {
    if (buffer instanceof Uint8Array) {
      buffer =
        buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength
          ? buffer.buffer
          : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
    } else if (Array.isArray(buffer)) {
      buffer = new Uint8Array(buffer).buffer;
    }
    const dv = new DataView(buffer);
    const magic = String.fromCharCode(dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3));
    if (magic !== expectedMagic) throw new Error("Unexpected preview payload format");
    return { buffer: buffer, dv: dv, gw: dv.getUint32(4, true), gh: dv.getUint32(8, true) };
  }

  function parsePayload(raw) {
    const { buffer, dv, gw, gh } = readPayloadHeader(raw, "APV1");
    const d = {
      gw,
      gh,
      minLat: dv.getFloat64(12, true),
      minLng: dv.getFloat64(20, true),
      maxLat: dv.getFloat64(28, true),
      maxLng: dv.getFloat64(36, true),
      minElev: dv.getFloat32(44, true),
      maxElev: dv.getFloat32(48, true),
      heights: new Uint16Array(buffer, 64, gw * gh),
    };
    d.mScale = d.maxElev > d.minElev ? (d.maxElev - d.minElev) / 65535 : 0;
    // Barely below the lowest terrain: the surroundings read as a flat plane

View on GitHub (pinned to 34048924d9)

Solutions

  1. Log the first bytes (or hex) of the returned payload and compare against the expected magic 'APV1'/'APL1' to identify what was actually returned.
  2. Rebuild/redeploy the Rust backend and GUI together so both sides agree on the payload magic and layout.
  3. Check the invoke call for typos in the command name and confirm the Rust command returns raw bytes (Vec<u8>), not a base64 string or JSON envelope.
  4. Add a version byte or handle multiple magics in readPayloadHeader if older payloads must still be supported.
  5. Catch this error at the call site and show a 'regenerate preview' affordance so stale cached payloads get refetched.

Example fix

// before
const raw = await window.__TAURI__.core.invoke("gui_get_preview_dem", { bboxText });
const data = parsePayload(raw); // throws if magic differs
// after
const raw = await window.__TAURI__.core.invoke("gui_get_preview_dem", { bboxText });
try {
  const data = parsePayload(raw);
} catch (e) {
  if (String(e.message).includes("Unexpected preview payload format")) {
    console.error("payload magic mismatch, head:", new TextDecoder().decode(raw.slice(0, 16)));
    invalidatePreviewCache();
    return null;
  }
  throw e;
}
Defensive patterns

Strategy: validation

Validate before calling

function hasValidMagic(raw, magic) {
  const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
  if (bytes.length < 4) return false;
  return String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]) === magic;
}
if (!hasValidMagic(raw, "APV1")) { refetchOrRegenerate(); return; }
const data = parsePayload(raw);

Type guard

function isBinaryPayload(v) { return v instanceof ArrayBuffer || v instanceof Uint8Array || Array.isArray(v); }

Try / catch

try {
  const data = parsePayload(raw);
} catch (e) {
  if (e.message === "Unexpected preview payload format") {
    console.error("magic head:", new TextDecoder().decode(raw.slice(0, 4)));
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling gui_get_preview_dem (or gui_get_preview_landcover) via window.__TAURI__.core.invoke and passing the raw result to parsePayload/readPayloadHeader when: the Rust command returned a legacy or different-magic payload version, the response was truncated to fewer than 4 bytes, an error string/object was returned instead of bytes, or the invoke result was base64-encoded/JSON-wrapped text rather than raw bytes.

Common situations: Frontend and backend out of sync after a partial deploy (old JS expecting APV1, Rust rebuilt with a new magic or layout); calling the command on a Tauri version or transport that serializes the byte array differently; a stale cached preview written by an older build; invoking the wrong command and feeding its output into parsePayload.

Related errors


AI-assisted analysis of louis-e/arnis@34048924d9 (2026-09-03). Data as JSON: /api/errors/1c77f32eecc65c98. Report an issue: GitHub.