mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' into doc/fix-tutorial-issues
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Serves a Meteor app over HTTP",
|
||||
version: "2.1.0",
|
||||
version: "2.1.1",
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
|
||||
@@ -681,11 +681,17 @@ WebAppInternals.staticFilesMiddleware = async function(
|
||||
// We cache them ~forever (1yr).
|
||||
const maxAge = info.cacheable ? 1000 * 60 * 60 * 24 * 365 : 0;
|
||||
|
||||
if (info.cacheable) {
|
||||
// Since we use req.headers["user-agent"] to determine whether the
|
||||
// client should receive modern or legacy resources, tell the client
|
||||
// to invalidate cached resources when/if its user agent string
|
||||
// changes in the future.
|
||||
// Resources whose URL already contains the content hash are immutable
|
||||
// and unique per architecture (modern vs legacy), so Vary: User-Agent
|
||||
// is unnecessary and harms CDN cache efficiency.
|
||||
//
|
||||
// If the requested URL does not contain the hash (e.g. development
|
||||
// or unhashed assets), we keep Vary: User-Agent to prevent cache
|
||||
// poisoning across different browsers.
|
||||
const includeVaryUserAgent =
|
||||
Meteor.settings.packages?.webapp?.includeVaryUserAgent ?? true;
|
||||
|
||||
if (info.cacheable && !pathname.includes(info.hash) && includeVaryUserAgent) {
|
||||
res.setHeader('Vary', 'User-Agent');
|
||||
}
|
||||
|
||||
|
||||
@@ -432,3 +432,158 @@ Tinytest.addAsync("webapp - parse url queries", async function (test) {
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
Tinytest.addAsync(
|
||||
'webapp - vary header optimization (hashed assets)',
|
||||
async function (test) {
|
||||
const arch = 'web.browser';
|
||||
const hash = 'js-hash-123';
|
||||
const hashedJs = `/optim-hashed.${hash}.js`;
|
||||
|
||||
WebAppInternals.staticFilesByArch[arch][hashedJs] = {
|
||||
content: 'console.log("prod")',
|
||||
absolutePath: '/tmp/mock-prod.js',
|
||||
cacheable: true,
|
||||
hash: hash,
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
try {
|
||||
const resJs = await asyncGet(Meteor.absoluteUrl(hashedJs));
|
||||
const varyJs = (resJs.headers['vary'] || '').toLowerCase();
|
||||
|
||||
test.isFalse(
|
||||
varyJs.includes('user-agent'),
|
||||
'Vary: User-Agent should be removed when the URL contains the file hash'
|
||||
);
|
||||
|
||||
} finally {
|
||||
delete WebAppInternals.staticFilesByArch[arch][hashedJs];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'webapp - vary header safety (unhashed assets)',
|
||||
async function (test) {
|
||||
const arch = 'web.browser';
|
||||
const unhashedJs = '/safety-unhashed.js';
|
||||
|
||||
WebAppInternals.staticFilesByArch[arch][unhashedJs] = {
|
||||
content: 'console.log("dev")',
|
||||
absolutePath: '/tmp/mock-dev.js',
|
||||
cacheable: true,
|
||||
hash: 'dev-internal-hash',
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await asyncGet(Meteor.absoluteUrl(unhashedJs));
|
||||
const varyHeader = (res.headers['vary'] || '').toLowerCase();
|
||||
|
||||
test.isTrue(
|
||||
varyHeader.includes('user-agent'),
|
||||
'Vary: User-Agent MUST be present when the URL does NOT contain the hash'
|
||||
);
|
||||
} finally {
|
||||
delete WebAppInternals.staticFilesByArch[arch][unhashedJs];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'webapp - vary header respects includeVaryUserAgent setting',
|
||||
async function (test) {
|
||||
const arch = 'web.browser';
|
||||
const unhashedJs = '/config-test.js';
|
||||
|
||||
const originalSettings = Meteor.settings.packages?.webapp?.includeVaryUserAgent;
|
||||
|
||||
if (!Meteor.settings.packages) Meteor.settings.packages = {};
|
||||
if (!Meteor.settings.packages.webapp) Meteor.settings.packages.webapp = {};
|
||||
|
||||
WebAppInternals.staticFilesByArch[arch][unhashedJs] = {
|
||||
content: 'console.log("config-test")',
|
||||
absolutePath: '/tmp/mock-config.js',
|
||||
cacheable: true,
|
||||
hash: 'internal-hash',
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
try {
|
||||
Meteor.settings.packages.webapp.includeVaryUserAgent = false;
|
||||
const resDisabled = await asyncGet(Meteor.absoluteUrl(unhashedJs));
|
||||
const varyDisabled = (resDisabled.headers['vary'] || '').toLowerCase();
|
||||
|
||||
test.isFalse(
|
||||
varyDisabled.includes('user-agent'),
|
||||
'Should NOT have Vary header when setting is false'
|
||||
);
|
||||
|
||||
Meteor.settings.packages.webapp.includeVaryUserAgent = true;
|
||||
const resEnabled = await asyncGet(Meteor.absoluteUrl(unhashedJs));
|
||||
const varyEnabled = (resEnabled.headers['vary'] || '').toLowerCase();
|
||||
|
||||
test.isTrue(
|
||||
varyEnabled.includes('user-agent'),
|
||||
'Should HAVE Vary header when setting is true'
|
||||
);
|
||||
|
||||
} finally {
|
||||
delete WebAppInternals.staticFilesByArch[arch][unhashedJs];
|
||||
Meteor.settings.packages.webapp.includeVaryUserAgent = originalSettings;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Verification: Ensure that a URL containing a specific hash serves the exact same
|
||||
// content and headers to all browsers (Modern vs Legacy).
|
||||
// This proves that removing 'Vary: User-Agent' is safe because the file content
|
||||
// is determined solely by the unique hash in the URL, not by the requesting browser.
|
||||
Tinytest.addAsync(
|
||||
'webapp - hashed files identical across user-agents',
|
||||
async function (test) {
|
||||
const arch = 'web.browser';
|
||||
const hash = 'unique-hash-999';
|
||||
const hashedPath = `/cdn-consistency-test.${hash}.js`;
|
||||
const url = Meteor.absoluteUrl(hashedPath);
|
||||
|
||||
WebAppInternals.staticFilesByArch[arch][hashedPath] = {
|
||||
content: 'console.log("consistent-cdn")',
|
||||
absolutePath: '/tmp/mock-consistent.js',
|
||||
cacheable: true,
|
||||
hash: hash,
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
try {
|
||||
const resModern = await asyncGet(url, {
|
||||
headers: { 'User-Agent': modernUserAgent }
|
||||
});
|
||||
|
||||
const resLegacy = await asyncGet(url, {
|
||||
headers: { 'User-Agent': legacyUserAgent }
|
||||
});
|
||||
|
||||
test.equal(
|
||||
resModern.content,
|
||||
resLegacy.content,
|
||||
'Hashed URLs must serve identical content to all browsers'
|
||||
);
|
||||
|
||||
const varyModern = (resModern.headers['vary'] || '').toLowerCase();
|
||||
const varyLegacy = (resLegacy.headers['vary'] || '').toLowerCase();
|
||||
|
||||
test.isFalse(
|
||||
varyModern.includes('user-agent'),
|
||||
'Modern browser request should not see Vary: User-Agent'
|
||||
);
|
||||
test.isFalse(
|
||||
varyLegacy.includes('user-agent'),
|
||||
'Legacy browser request should not see Vary: User-Agent'
|
||||
);
|
||||
} finally {
|
||||
delete WebAppInternals.staticFilesByArch[arch][hashedPath];
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -103,6 +103,28 @@ We're using the [connect-route](https://www.npmjs.com/package/connect-route) NPM
|
||||
|
||||
And finally, if you decide to use this technique you'll want to make sure you understand how conflicting client side routing will affect user experience.
|
||||
|
||||
### Static Assets & Caching
|
||||
|
||||
By default, Meteor serves different bundles (Modern vs. Legacy) based on the user's browser. Historically, this required the `Vary: User-Agent` header on all responses, which forced CDNs to fragment their cache (storing separate copies for Chrome, Safari, etc.).
|
||||
|
||||
`webapp` now includes an automatic optimization to solve this:
|
||||
|
||||
* **Production (Hashed Filenames):** Files with the hash in the pathname (e.g., `/app.abc12345.js`) are served **without** the `Vary: User-Agent` header. Since the pathname uniquely identifies the content, CDNs can safely cache a single copy for all users.
|
||||
* **Development (Unhashed Filenames):** Files without the hash in the pathname (e.g., `/packages/promise.js?hash=abc123`) **keep** the `Vary: User-Agent` header to ensure safety during development.
|
||||
|
||||
This behavior is enabled by default. You can control it via `Meteor.settings`:
|
||||
```json
|
||||
{
|
||||
"packages": {
|
||||
"webapp": {
|
||||
"includeVaryUserAgent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Setting `includeVaryUserAgent` to `false` will disable the header for **all** static files.
|
||||
|
||||
### React SSR Optimization (Meteor 3.4)
|
||||
|
||||
**Experimental: Disable Boilerplate Response** ([PR#13855](https://github.com/meteor/meteor/pull/13855))
|
||||
|
||||
Reference in New Issue
Block a user