跪拜 Guibai
← Back to the summary

A Build Plugin That Stamps Every HTTP Request with the Frontend Version

unplugin-version-injector 2.2 released: one-time integration, full support for Vite / Webpack / Rspack / Rollup / Rolldown, automatically injecting version number and build time into the page and request headers.

A Scenario Every Frontend Developer Has Encountered

A strange bug is reported in production. You can't reproduce it locally. You ask the user, and they say: "It's fine on my end."

Here's the problem — you have no idea which frontend version they are currently running:

You check the backend logs, and rows of requests are neatly lined up, but none of them can tell you "which frontend build sent this." Troubleshooting shifts from "reading code" to "guessing the version."

The essence of this problem is: the frontend's version information does not flow along with the request to the backend.

unplugin-version-injector aims to solve this. Version 2.2 completes this task.

Get Started in 30 Seconds

pnpm add -D unplugin-version-injector

One-line integration based on your build tool (subpaths correspond to each adapter):

// Vite
import versionInjector from 'unplugin-version-injector/vite';
export default { plugins: [versionInjector()] };
// Webpack 4/5
const versionInjector = require('unplugin-version-injector/webpack');
module.exports = { plugins: [versionInjector()] };
// Rspack / Rollup / Rolldown — same pattern
const versionInjector = require('unplugin-version-injector/rspack');

Without any parameters, it automatically reads the nearest package.json and injects the following into the output HTML:

<head>
  <meta name="version" content="my-app/1.2.3">
  <meta name="project" content="my-app">
</head>

And prints a styled build information banner in the console that automatically adapts to light/dark mode:

 [email protected]
 Build Time: 2024-04-01 12:00:00

Open the production page and hit F12; the version is clear at a glance. That's the basic operation. The feature that truly solves the initial pain point is below.

⭐ 2.2 Main Feature: Let Every Request Report Its Own Version

Enable requestHeaders, and the plugin injects a script at the very beginning of the page that patches window.fetch and XMLHttpRequest, automatically attaching two headers to requests:

versionInjector({ requestHeaders: true });

After this, every request will carry:

X-Client-Version: my-app/1.2.3
X-Client-Build-Time: 2024-04-01 12:00:00

From now on, every request in the backend logs comes with its own frontend version. To locate a production issue, simply filter logs by version number — no more guessing, no more asking users to clear their cache.

Because it intercepts the low-level fetch / XHR, mainstream libraries like axios and umi-request all work automatically, with zero changes to your business code.

Full Configuration Scenarios for requestHeaders

requestHeaders is disabled by default and, by default, only injects headers into same-origin requests (safe, avoids unnecessary cross-origin preflight requests). Let's walk through all scenarios.

Scenario 1: Same-Origin

The page and API are on the same domain, or a local dev-server proxy is used (requests go to /api, which is same-origin from the browser's perspective):

versionInjector({ requestHeaders: true }); // true = default config, same-origin only

Scenario 2: Custom Header Names

versionInjector({
  requestHeaders: {
    versionHeaderName: 'X-App-Version',
    buildTimeHeaderName: 'X-App-Build',
  },
});

Scenario 3: A Single Cross-Origin API (Most Common in Production)

The frontend and API are on different origins; add the domain to include (strings match by URL prefix):

versionInjector({
  requestHeaders: { include: ['https://api.example.com'] },
});

Scenario 4: Multiple Cross-Origin APIs / Regex Batch Matching

versionInjector({
  requestHeaders: {
    include: [
      'https://api.example.com',
      'https://auth.example.com',
      /^https:\/\/[^/]*\.example\.com\//, // all subdomains of *.example.com
    ],
  },
});

Scenario 5: Monorepo + Fully Cross-Origin (The Hardest, and Most Worth Discussing) ⭐

This is the scenario I've encountered most in real projects: multiple sub-applications within a monorepo share a single build configuration, and each application connects to cross-origin APIs in different environments (dev / sandbox / prod). Hardcoding domain names is simply unmaintainable.

Two key points:

① Write include only once in the shared root configuration, and all sub-packages inherit it. Don't write it separately in each sub-application.

② Domains change with the environment; don't hardcode them — use environment variables to dynamically construct them. Each sub-application's .env.* usually already contains API domain variables; just read them directly:

// Shared root build configuration (webpack example)
const versionInjector = require('unplugin-version-injector/webpack');

// These variables are provided by each sub-application's / environment's .env
const apiOrigins = [
  process.env.VUE_APP_API_ORIGIN,
  process.env.VUE_APP_SDK_API_ORIGIN,
  process.env.VUE_APP_USER_API_ORIGIN,
].filter(Boolean); // remove undefined ones

module.exports = {
  configureWebpack: {
    plugins: [
      versionInjector({ requestHeaders: { include: apiOrigins } }),
    ],
  },
};

This way, dev / sandbox / prod each get their corresponding domains, automatically switching at build time, with a single configuration working across all environments.

If all your APIs are under a few fixed primary domains, you can also wrap it up with a single regex (new subdomains are automatically matched):

versionInjector({
  requestHeaders: {
    include: [/^https:\/\/[^/]*\.(example\.io|example\.dev|sandbox-example\.com)\//],
  },
});

Only put in the API domains where you initiate fetch/XHR and can modify CORS. Do not add CDN or third-party SDK script domains (whose CORS you cannot control); adding them will only trigger preflight requests and break resource loading.

⚠️ Cross-Origin Must-Read: Backend Cooperation Required

Adding custom headers to cross-origin requests causes the browser to send an OPTIONS preflight request first. Every API service matched by include must allow these two headers in its response:

Access-Control-Allow-Headers: X-Client-Version, X-Client-Build-Time

Any backend that isn't configured will have its requests blocked by the browser. You must confirm this for each backend — this is also why cross-origin injection must be explicitly enabled via include rather than being on by default.

Troubleshooting Tip: Same-Origin or Cross-Origin?

Open the Network tab and look at the request's actual URL:

A common pitfall: locally via proxy it's same-origin, but in production it's a direct cross-origin connection. So it works fine locally, but the headers disappear after deployment — nine times out of ten, the production cross-origin domain wasn't added to include, or the backend didn't allow the headers.

Other Updates in 2.2

A Few Details That Give Me Peace of Mind

When writing this plugin, a few points were deliberately polished:

Conclusion

Version injection is a small thing, but "not knowing which version a user is running in production" is a big thing. unplugin-version-injector 2.2 turns it into a complete solution: one-time integration, universal across build tools, and capable of carrying information all the way to backend logs.

If it saves you a round of "guessing the version" troubleshooting, a Star ⭐ is welcome.