Hono not picking up MIME types correctly (I think)
What version of Hono are you using?
4.6.12
What runtime/platform is your app running on? (with version if possible)
Node 22 LTS
What steps can reproduce the bug?
- Set up static serving for an SPA (rerouting all requests to
index.html) - Hit the endpoint and see that the page returns with a
Content-Typeoftext/plain; charset=utf-8, nottext/html.
import { Hono } from 'hono';
import { serveStatic } from "@hono/node-server/serve-static";
const app = new Hono();
app.use('/*', serveStatic({ root: './path/to/public', rewriteRequestPath: () => 'index.html' }));
serve({ fetch: app.fetch });
What is the expected behavior?
When I try to make a request to the server on any path (per my example code), I should get the content returned as text/html, not text/plain.
What do you see instead?
The content comes through as text/plain, which prevents it from rendering in the browser correctly.
Additional information
The code I'm running is being executed in a nodejs:22-slim Docker image on Fly.io.
- Source code for my specific app is here: https://github.com/recipes-blue/recipes.blue
- Tracking issue in my repo: hbjydev/cookware#1
- Live version for you to see the problem in action: https://cookware.fly.dev
Hi, I'm currently working on this issue. I could reproduce the wrong mime-type behavior. I will continue to investigate the cause.
Hi, I'm currently working on this issue. I could reproduce the wrong mime-type behavior. I will continue to investigate the cause.
Awesome, thanks! :)
@sushichan044 Thanks! Please do it.
Hello, any workaround while waiting for a fix ? I have to deploy the app 😅
For now I've done that but if you reload the page on a different route it doesn't work
import { serveStatic } from "hono/serve-static";
app.use(
"*",
serveStatic({
root: "./dist",
getContent: async (path, c) => {
try {
const data = await fs.readFile(path);
let contentType = "text/plain";
if (path.endsWith(".html")) {
contentType = "text/html";
} else if (path.endsWith(".js")) {
contentType = "application/javascript";
} else if (path.endsWith(".css")) {
contentType = "text/css";
} else if (path.endsWith(".json")) {
contentType = "application/json";
} else if (path.endsWith(".png")) {
contentType = "image/png";
} else if (path.endsWith(".jpg") || path.endsWith(".jpeg")) {
contentType = "image/jpeg";
}
return new Response(data, {
headers: {
"Content-Type": contentType,
},
});
} catch (error) {
return null;
}
},
})
);
This happens with different combinations of middleware that modify the c.header,
and the investigation takes a long time. Please give me some more time.
@sushichan044 Any progress?
@yusukebe Sorry, I had investigated this before but got busy and had to suspend it. I almost know the cause and will report back in a few days.
@sushichan044 Okay! Thanks!
I would like to add that .webmanifest should have application/manifest+json as content type but currently return as application/octet-stream.
@sep2 The MIME type handling in the webmanifest is a different cause than this issue, but it should be a bug. I'll create a PR to resolve it.
@yusukebe
I have created a repository contains a minimal reproduction code and a detailed explanation of the cause. https://github.com/sushichan044/hono-header-handling
The explanation may be complicated, but would you please read it in your spare time?
I think there are two ways to fix this.
First, special handling of content-type in this branch would be an ad hoc approach.
https://github.com/honojs/hono/blob/5ca6c6ef867e022671b4c429c04d0ff89ed0c37c/src/context.ts#L670-L676
Specifically, if Context.#headers already contains content-type, we do not overwrite the header.
This prevents overwriting the expected header content by the content-type: text/plain implicit in the 404 response when Context.#res is initialized.
Since only the content-type header may be implicitly included in Context.#res.headers, and the rest should be explicitly written from the Middleware or application code, this handling should not be a problem.
However, in edge cases, implementations that override the content-type set in c.header with c.res.headers.set will not work as expected.
Another approach would be to change the way response headers are stored to make the structure less prone to unexpected overwrites.
However, I have no specific idea yet. I think the scope of impact is also larger than the first way.
To simplify the implementation, it is possible to stop using preparedHeaders, but this may compromise performance.
In addition, I will document the differences between the two methods of editing headers, c.header and c.res.headers.
Or perhaps we should create a best practice guide when implementing Middlewares 🤔
@sushichan044
I have created a repository contains a minimal reproduction code and a detailed explanation of the cause. https://github.com/sushichan044/hono-header-handling
The explanation may be complicated, but would you please read it in your spare time?
Ops. This is a bug! I'll take a look. Thank you so much!
Maybe this is a super minimal reproduce code:
import { Hono } from 'hono'
const app = new Hono()
app.use(async (c, next) => {
c.header('foo', 'bar', { append: true })
c.res // call Context.res()
await next()
})
app.get('/', (c) => {
c.header('Content-Type', 'text/html; charset=UTF-8')
return c.body('<html><body><h1>Hello World</h1></body></html>')
})
const res = await app.request('/')
console.log(res.headers.get('content-type')) // ❌️ text/plain
Current workaround by adding a middleware as follow
.use('*',
async (c, next) => {
await next()
const path = c.req.path
if (!path.includes('.') && !path.startsWith(`/assets`)) {
c.header('Content-Type', 'text/html')
}
},
serveStatic({
root: './web/dist',
rewriteRequestPath: (path) => {
if (!path.includes('.') && !path.startsWith(`/assets`)) {
return '/index.html'
}
return path
}
}))
Oh, I've found the same issue, the problem is
I've used, it will serve correctly,
import { serveStatic } from "hono/bun";
// Serve static files from the frontend build directory
app.get("*", serveStatic({ root: FRONTEND_PATH }));
// Fallback route to serve the index.html for SPA routing
app.get("*", serveStatic({ root: FRONTEND_PATH, path: "index.html" }));
after i've make it like on /app, all mime type are override to be Content-Type: text/html
import { serveStatic } from "hono/bun";
// Serve static files from the frontend build directory
app.get("/app/*", serveStatic({ root: FRONTEND_PATH }));
// Fallback route to serve the index.html for SPA routing
app.get("/app/*", serveStatic({ root: FRONTEND_PATH, path: "index.html" }));
Note: hono 4.7.11
I've never investigated well, but these problems may be fixed with the latest version 4.8.0. Can you try it?
Still an Issue as of version 4.8.3
I tried the cases that were commented on this issue:
- https://github.com/honojs/hono/issues/3736#issue-2726757587
- https://github.com/honojs/hono/issues/3736#issuecomment-2814695601
- https://github.com/honojs/hono/issues/3736#issuecomment-2817946197
- https://github.com/honojs/hono/issues/3736#issuecomment-2973609493
I can confirm these issues are fixed with the latest 4.8.3. I'd like to close this issue now. If you encounter any problems, please create a new issue with instructions for reproduction. We can work on fixing it.
Thanks.