Getting 404 Errors After Building a Teams Tab App? HTML Caching Might Be the Cause
Introduction
When developing Microsoft Teams tab apps, there's a frustrating issue you may run into during local development.
After modifying frontend code and rebuilding, Vite/Rollup adds a content hash to filenames (e.g., TeamsInitializer.bBRVpIft.js). However, if the browser (Teams WebView) has cached the HTML, the old HTML continues to reference the old hashed filenames, which results in 404 errors.
In this article, we'll share a solution we discovered by digging into the internal structure of the Teams SDK v2 local server.
Common Solutions and Their Drawbacks
Approach 1: Remove Hashes from Filenames
// astro.config.mjs or vite.config.js
export default {
vite: {
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].[ext]',
},
},
},
},
};
Drawback: Removing hashes means the browser may cache files indefinitely, preventing code updates from being reflected. You lose the benefits of cache busting.
Approach 2: Use the Dev Server (HMR)
Using Vite or Astro's dev server with HMR (Hot Module Replacement) avoids caching issues altogether.
Drawback: Teams apps are typically configured so that the Teams client (WebView) accesses a specific port, making it difficult to use the dev server's separate port directly.
Investigating the Teams SDK v2 Internals
When looking into the @microsoft/teams.apps package in Teams SDK v2, we discovered that HttpPlugin uses Express internally.
// Excerpt from node_modules/@microsoft/teams.apps/dist/plugins/http/plugin.js
const express_1 = __importDefault(require("express"));
let HttpPlugin = class HttpPlugin {
constructor(server, options) {
this.express = (0, express_1.default)();
// ...
this.use = this.express.use.bind(this.express); // ← use() is exposed!
}
static(path, dist) {
this.express.use(path, express_1.default.static(dist));
return this;
}
}
Key findings:
-
HttpPluginholds an internal Express instance - The
use()method is exposed and bound to Express'sapp.use() - This means we can add arbitrary Express middleware
Solution: Set Cache-Control Headers via Middleware
Leveraging this discovery, we add middleware to disable HTML caching only during local development.
// src/index.ts
import fs from "fs";
import https from "https";
import path from "path";
import { App, HttpPlugin, IPlugin } from "@microsoft/teams.apps";
import { ConsoleLogger } from "@microsoft/teams.common/logging";
const sslOptions = {
key: process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined,
cert: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined,
};
const httpPlugin = new HttpPlugin(
sslOptions.cert && sslOptions.key ? https.createServer(sslOptions) : undefined
);
// Disable HTML caching for local development only
if (!process.env.RUNNING_ON_AZURE) {
httpPlugin.use("/tabs", (req, res, next) => {
// Disable caching for HTML files and index.html requests
if (req.path.endsWith(".html") || req.path === "/" || !req.path.includes(".")) {
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
next();
});
}
const plugins: IPlugin[] = [httpPlugin];
const app = new App({
logger: new ConsoleLogger("tab", { level: "debug" }),
plugins: plugins,
});
app.tab("home", path.join(__dirname, "./client"));
(async () => {
await app.start(+(process.env.PORT || 3978));
})();
Key Points
-
Environment-based control: The middleware is only added when the
RUNNING_ON_AZUREenvironment variable is not set (i.e., during local development) -
Path matching: Paths ending in
.html, the root path, and paths without extensions (SPA routing) are treated as HTML requests -
Cache-busting headers: Three headers —
Cache-Control,Pragma, andExpires— are set to ensure caching is fully disabled
Benefits of This Approach
- Hashed filenames are preserved: JS/CSS files keep their hashes, so production environments' caching efficiency is unaffected
- No impact on production environments: Since it's controlled by an environment variable, the middleware is not added during Azure deployments
- Extensible: The same technique can be used to add logging, authentication, custom headers, and more
Use Case: Adding Other Middleware
Using this technique, you can add various middleware to the Teams SDK.
// Add request logging
httpPlugin.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// Add custom headers
httpPlugin.use((req, res, next) => {
res.setHeader("X-Custom-Header", "my-value");
next();
});
// Add authentication for specific paths
httpPlugin.use("/api/private", (req, res, next) => {
if (!req.headers.authorization) {
res.status(401).send("Unauthorized");
return;
}
next();
});
Caveats
- This approach relies on the internal implementation details of Teams SDK v2 (
@microsoft/teams.appsv2.x) - The API may change in future versions
- This usage is not documented in the official documentation
Conclusion
We discovered that Teams SDK v2's HttpPlugin uses Express internally and exposes the use() method. By taking advantage of this:
- HTML caching issues during local development are resolved
- Cache-busting via hashed filenames is preserved
- Development experience is improved without affecting production environments
We hope this helps anyone working with Teams SDK v2 who runs into the same issue.
This article was originally published in Japanese at archelon-inc.jp.
Top comments (0)