From 76bb83e514798a956b2026bfaa8941ea5b32106b Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sun, 4 Oct 2026 22:22:37 +0900 Subject: [PATCH 1/2] fix(serve-static): only serve GET and HEAD requests --- src/serve-static.ts | 6 ++++- test/serve-static.test.ts | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/serve-static.ts b/src/serve-static.ts index fe3d5fc..cf8b8ca 100644 --- a/src/serve-static.ts +++ b/src/serve-static.ts @@ -122,6 +122,10 @@ export const serveStatic = ( return next() } + if (c.req.method !== 'GET' && c.req.method !== 'HEAD') { + return next() + } + let filename: string if (optionPath) { @@ -195,7 +199,7 @@ export const serveStatic = ( const range = c.req.header('range') || '' c.header('Last-Modified', stats.mtime.toUTCString()) - if (c.req.method == 'HEAD' || c.req.method == 'OPTIONS') { + if (c.req.method === 'HEAD') { c.header('Content-Length', size.toString()) c.status(200) result = c.body(null) diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index 5f0960d..ab8877b 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -1,6 +1,8 @@ import { Hono } from 'hono' +import type * as fs from 'node:fs' import { chmodSync, + createReadStream, mkdirSync, mkdtempSync, rmSync, @@ -14,6 +16,15 @@ import { serveStatic } from './../src/serve-static' import { createAdaptorServer } from './../src/server' import { requestServer } from './helpers/request' +vi.mock('node:fs', async (importOriginal) => { + const originalFs = await importOriginal() + return { + ...originalFs, + statSync: vi.fn(originalFs.statSync), + createReadStream: vi.fn(originalFs.createReadStream), + } +}) + describe('Serve Static Middleware', () => { const app = new Hono<{ Variables: { @@ -76,6 +87,52 @@ describe('Serve Static Middleware', () => { const server = createAdaptorServer(app) + describe('HTTP methods', () => { + beforeEach(() => { + vi.mocked(statSync).mockClear() + vi.mocked(createReadStream).mockClear() + }) + + it.each(['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'PROPFIND'])( + 'Should pass %s requests to the next handler without accessing files', + async (method) => { + const rewriteRequestPath = vi.fn((path: string) => path) + const onFound = vi.fn() + const onNotFound = vi.fn() + const app = new Hono().use( + '/static/*', + serveStatic({ root: './test/assets', rewriteRequestPath, onFound, onNotFound }) + ) + app.on(method, '/static/plain.txt', (c) => c.text('Handled downstream', 202)) + + const res = await requestServer(createAdaptorServer(app), { + method, + path: '/static/plain.txt', + }) + + expect(res.status).toBe(202) + expect(await res.text()).toBe('Handled downstream') + expect(rewriteRequestPath).not.toHaveBeenCalled() + expect(statSync).not.toHaveBeenCalled() + expect(createReadStream).not.toHaveBeenCalled() + expect(onFound).not.toHaveBeenCalled() + expect(onNotFound).not.toHaveBeenCalled() + } + ) + + it.each(['/static/plain.txt', '/static/does-not-exist.html'])( + 'Should return 404 for an unhandled POST request regardless of file existence - %s', + async (path) => { + const res = await requestServer(server, { method: 'POST', path }) + + expect(res.status).toBe(404) + expect(await res.text()).toBe('404 Not Found') + expect(statSync).not.toHaveBeenCalled() + expect(createReadStream).not.toHaveBeenCalled() + } + ) + }) + it('Should return index.html', async () => { const res = await requestServer(server, { method: 'GET', path: '/static/' }) expect(res.status).toBe(200) From b2eb82050c8b4bed8a606d053e89da58b833a60b Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sun, 4 Oct 2026 22:22:37 +0900 Subject: [PATCH 2/2] docs(serve-static): document supported HTTP methods --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a951cd..25af664 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,8 @@ app.use('/static/*', serveStatic({ root: './static' })) Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`. +Only `GET` and `HEAD` requests are served. Requests with other methods are passed to the next middleware. + ### Options #### `allowPercentInPath`