← ./articles

Test Tauri v2 Updater Locally with a Mock Server

A Tauri updater test can fail quietly. The application starts, the update banner never appears, and there is no useful UI error. That does not always mean your updater code is wrong.

For a local test, you need four things to line up: signed updater artifacts, a valid updater JSON response, a mock server that the WebView can fetch, and a test-only setting that permits local HTTP.

This article shows a local test loop that gives you request-level evidence instead of guessing from the UI.

The symptom: update checks return no banner and no clear error

The common failure looks like this:

  • check() runs from the frontend or Rust side
  • the application stays on the current version
  • no update banner appears
  • the mock server receives no request, or receives only one request
  • the production updater endpoint is not involved

This symptom has several possible causes. The updater artifact may not be signed. The JSON may contain the wrong signature value. The WebView may block the mock server because of CORS. Tauri may reject the endpoint because it is HTTP instead of HTTPS.

Do not start by rewriting the UI. First prove where the request stops.

Build a signed updater artifact before testing

Tauri updater installations require signatures. A normal installer is not enough for a realistic updater test.

Use a release build command that creates updater artifacts:

npm run tauri -- build --features updater --config src-tauri/tauri.updater.conf.json

Then check that the expected signature exists:

Get-ChildItem src-tauri\target\release\bundle\nsis -Filter *.sig

If there is no .sig file, stop. A mock server cannot fix a missing updater artifact.

Serve updater JSON and installer files from a local mock

Use a small server that logs each request. The log is the first piece of evidence.

const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");

const port = 8899;
const bundleDir = path.join("src-tauri", "target", "release", "bundle", "nsis");
const installerName = "my-app_1.0.1_x64-setup.exe";
const signatureName = `${installerName}.sig`;

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);

  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");

  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  if (req.url === "/updater.json") {
    const signature = fs
      .readFileSync(path.join(bundleDir, signatureName), "utf8")
      .trim();

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      version: "1.0.1",
      notes: "Local updater test",
      pub_date: new Date().toISOString(),
      platforms: {
        "windows-x86_64": {
          signature,
          url: `http://127.0.0.1:${port}/${installerName}`
        }
      }
    }));
    return;
  }

  if (req.url === `/${installerName}`) {
    const installerPath = path.join(bundleDir, installerName);
    res.writeHead(200, {
      "Content-Type": "application/octet-stream",
      "Content-Length": fs.statSync(installerPath).size
    });
    fs.createReadStream(installerPath).pipe(res);
    return;
  }

  res.writeHead(404);
  res.end("not found");
});

server.listen(port, "127.0.0.1", () => {
  console.log(`Updater mock server listening on http://127.0.0.1:${port}`);
});

The CORS headers are not decorative. The updater request is made from the WebView runtime, so a local server that works in curl may still fail in the app if the browser layer blocks it.

Add temporary HTTP-only test config

Tauri updater endpoints should use HTTPS in production. For a local mock server, use a test-only config overlay:

{
  "plugins": {
    "updater": {
      "endpoints": ["http://127.0.0.1:8899/updater.json"],
      "dangerousInsecureTransportProtocol": true
    }
  }
}

Keep this setting out of production release config. The name is accurate: it allows insecure transport. It is useful for a local test and inappropriate for real distribution.

If you need repeatable tests, name the file clearly:

src-tauri/tauri.updater.local-test.conf.json

Then build the local test variant with both the updater overlay and the local endpoint overlay if your build tool supports multiple config inputs, or with a dedicated combined test config.

Verify requests, download, install, and relaunch

Run the test in stages.

First, confirm the manifest request:

GET /updater.json

If the mock server does not log this request, the app did not reach your endpoint. Check endpoint config, feature gating, plugin initialization, and HTTP allowance.

Second, confirm the installer request:

GET /my-app_1.0.1_x64-setup.exe

If only the manifest request appears, check the JSON fields:

  • version is greater than the installed app version
  • platforms.windows-x86_64.url points to the installer or updater bundle you serve
  • platforms.windows-x86_64.signature contains the .sig file content
  • the mock server returns 200

Third, confirm install behavior. On Windows, the updater installation step can exit the application. If your UI disappears during install, that may be expected. Verify by checking whether the new installed version starts after relaunch.

Keep the mock test separate from release config

A good updater test setup has a hard boundary:

  • production config uses HTTPS
  • local mock config may use HTTP
  • production config never enables dangerousInsecureTransportProtocol
  • local mock server logs every request
  • test artifacts are disposable

Do not keep the local endpoint in the same file used for release builds. That creates a release risk that is easy to miss during a rushed deploy.

Troubleshooting checklist

Use this order before changing application code:

  1. Does the build produce a .sig file?
  2. Does the mock server start and serve /updater.json?
  3. Does the app request /updater.json?
  4. Does the app request the installer or updater bundle URL?
  5. Does the version in JSON exceed the installed app version?
  6. Does the signature field contain file content, not a path?
  7. Is HTTP enabled only in the local test config?
  8. Does the app relaunch or otherwise show the new version after install?

Each step narrows the failure. Without this separation, a missing signature can look like a UI bug, and a CORS failure can look like a bad updater API call.

References

Summary

A local updater test should prove the network path, the manifest, the signed artifact, and the install step. Use a mock server with CORS headers, enable HTTP only in a test config, and rely on request logs before trusting the UI.