diff --git a/deno_dist/adapter/deno/serve-static.ts b/deno_dist/adapter/deno/serve-static.ts index 4a8250bb1..8536bcd62 100644 --- a/deno_dist/adapter/deno/serve-static.ts +++ b/deno_dist/adapter/deno/serve-static.ts @@ -11,6 +11,7 @@ export type ServeStaticOptions = { root?: string path?: string rewriteRequestPath?: (path: string) => string + onNotFound?: (path: string) => void | Promise } const DEFAULT_DOCUMENT = 'index.html' @@ -50,10 +51,10 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }) => { } // Return Response object with stream return c.body(file.readable) - } else { - console.warn(`Static file: ${path} is not found`) - await next() } + + await options.onNotFound?.(path) + await next() return } } diff --git a/runtime_tests/bun/index.test.tsx b/runtime_tests/bun/index.test.tsx index cb32e54df..9264623ac 100644 --- a/runtime_tests/bun/index.test.tsx +++ b/runtime_tests/bun/index.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test' +import { describe, expect, it, mock, beforeEach } from 'bun:test' import { serveStatic } from '../../src/adapter/bun' import { Context } from '../../src/context' import { env, getRuntimeKey } from '../../src/helper/adapter' @@ -76,10 +76,11 @@ describe('Basic Auth Middleware', () => { describe('Serve Static Middleware', () => { const app = new Hono() + const onNotFound = mock(() => {}) app.all('/favicon.ico', serveStatic({ path: './runtime_tests/bun/favicon.ico' })) app.all( '/favicon-notfound.ico', - serveStatic({ path: './runtime_tests/bun/favicon-notfound.ico' }) + serveStatic({ path: './runtime_tests/bun/favicon-notfound.ico', onNotFound }) ) app.use('/favicon-notfound.ico', async (c, next) => { await next() @@ -89,6 +90,7 @@ describe('Serve Static Middleware', () => { '/static/*', serveStatic({ root: './runtime_tests/bun/', + onNotFound, }) ) app.get( @@ -99,6 +101,8 @@ describe('Serve Static Middleware', () => { }) ) + beforeEach(() => onNotFound.mockClear()) + it('Should return static file correctly', async () => { const res = await app.request(new Request('http://localhost/favicon.ico')) await res.arrayBuffer() @@ -110,12 +114,14 @@ describe('Serve Static Middleware', () => { const res = await app.request(new Request('http://localhost/favicon-notfound.ico')) expect(res.status).toBe(404) expect(res.headers.get('X-Custom')).toBe('Bun') + expect(onNotFound).toHaveBeenCalledWith('./runtime_tests/bun/favicon-notfound.ico') }) it('Should return 200 response - /static/plain.txt', async () => { const res = await app.request(new Request('http://localhost/static/plain.txt')) expect(res.status).toBe(200) expect(await res.text()).toBe('Bun!') + expect(onNotFound).not.toHaveBeenCalled() }) it('Should return 200 response - /dot-static/plain.txt', async () => { diff --git a/runtime_tests/deno/deps.ts b/runtime_tests/deno/deps.ts index 11a7a8a47..22cb44556 100644 --- a/runtime_tests/deno/deps.ts +++ b/runtime_tests/deno/deps.ts @@ -1 +1,2 @@ export { assert, assertEquals, assertMatch } from 'https://deno.land/std@0.147.0/testing/asserts.ts' +export { assertSpyCall, assertSpyCalls, spy } from 'https://deno.land/std@0.147.0/testing/mock.ts' diff --git a/runtime_tests/deno/middleware.test.tsx b/runtime_tests/deno/middleware.test.tsx index 0e6784969..8dd5d6909 100644 --- a/runtime_tests/deno/middleware.test.tsx +++ b/runtime_tests/deno/middleware.test.tsx @@ -2,7 +2,7 @@ /** @jsxFrag Fragment */ import { basicAuth, jsx, Fragment, serveStatic, jwt } from '../../deno_dist/middleware.ts' import { Hono } from '../../deno_dist/mod.ts' -import { assertEquals, assertMatch } from './deps.ts' +import { assertEquals, assertMatch, assertSpyCall, assertSpyCalls, spy } from './deps.ts' // Test just only minimal patterns. // Because others are already tested well in Cloudflare Workers environment. @@ -66,10 +66,11 @@ Deno.test('JSX middleware', async () => { Deno.test('Serve Static middleware', async () => { const app = new Hono() + const onNotFound = spy(() => {}) app.all('/favicon.ico', serveStatic({ path: './runtime_tests/deno/favicon.ico' })) app.all( '/favicon-notfound.ico', - serveStatic({ path: './runtime_tests/deno/favicon-notfound.ico' }) + serveStatic({ path: './runtime_tests/deno/favicon-notfound.ico', onNotFound }) ) app.use('/favicon-notfound.ico', async (c, next) => { await next() @@ -80,6 +81,7 @@ Deno.test('Serve Static middleware', async () => { '/static/*', serveStatic({ root: './runtime_tests/deno', + onNotFound, }) ) @@ -100,6 +102,9 @@ Deno.test('Serve Static middleware', async () => { assertEquals(res.status, 404) assertMatch(res.headers.get('Content-Type') || '', /^text\/plain/) assertEquals(res.headers.get('X-Custom'), 'Deno') + assertSpyCall(onNotFound, 0, { + args: ['./runtime_tests/deno/favicon-notfound.ico'], + }) res = await app.request('http://localhost/static/plain.txt') assertEquals(res.status, 200) @@ -108,6 +113,7 @@ Deno.test('Serve Static middleware', async () => { res = await app.request('http://localhost/dot-static/plain.txt') assertEquals(res.status, 200) assertEquals(await res.text(), 'Deno!!') + assertSpyCalls(onNotFound, 1) }) Deno.test('JWT Authentication middleware', async () => { diff --git a/src/adapter/bun/serve-static.ts b/src/adapter/bun/serve-static.ts index e8372771a..084277a77 100644 --- a/src/adapter/bun/serve-static.ts +++ b/src/adapter/bun/serve-static.ts @@ -13,6 +13,7 @@ export type ServeStaticOptions = { root?: string path?: string rewriteRequestPath?: (path: string) => string + onNotFound?: (path: string) => void | Promise } const DEFAULT_DOCUMENT = 'index.html' @@ -49,7 +50,7 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }) => { } } - console.warn(`Static file: ${path} is not found`) + await options.onNotFound?.(path) await next() return } diff --git a/src/adapter/cloudflare-workers/serve-static.test.ts b/src/adapter/cloudflare-workers/serve-static.test.ts index 17d6630b9..2d1306850 100644 --- a/src/adapter/cloudflare-workers/serve-static.test.ts +++ b/src/adapter/cloudflare-workers/serve-static.test.ts @@ -31,7 +31,8 @@ Object.assign(global, { describe('ServeStatic Middleware', () => { const app = new Hono() - app.use('/static/*', serveStatic({ root: './assets' })) + const onNotFound = vi.fn(() => {}) + app.use('/static/*', serveStatic({ root: './assets', onNotFound })) app.use('/static-no-root/*', serveStatic()) app.use( '/dot-static/*', @@ -41,11 +42,14 @@ describe('ServeStatic Middleware', () => { }) ) + beforeEach(() => onNotFound.mockClear()) + it('Should return plain.txt', async () => { const res = await app.request('http://localhost/static/plain.txt') expect(res.status).toBe(200) expect(await res.text()).toBe('This is plain.txt') expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8') + expect(onNotFound).not.toHaveBeenCalled() }) it('Should return hono.html', async () => { @@ -53,11 +57,13 @@ describe('ServeStatic Middleware', () => { expect(res.status).toBe(200) expect(await res.text()).toBe('

Hono!

') expect(res.headers.get('Content-Type')).toBe('text/html; charset=utf-8') + expect(onNotFound).not.toHaveBeenCalled() }) it('Should return 404 response', async () => { const res = await app.request('http://localhost/static/not-found.html') expect(res.status).toBe(404) + expect(onNotFound).toHaveBeenCalledWith('assets/static/not-found.html') }) it('Should return plan.txt', async () => { diff --git a/src/adapter/cloudflare-workers/serve-static.ts b/src/adapter/cloudflare-workers/serve-static.ts index 241138c27..2bcbd1a27 100644 --- a/src/adapter/cloudflare-workers/serve-static.ts +++ b/src/adapter/cloudflare-workers/serve-static.ts @@ -11,6 +11,7 @@ export type ServeStaticOptions = { manifest?: object | string namespace?: KVNamespace rewriteRequestPath?: (path: string) => string + onNotFound?: (path: string) => void | Promise } const DEFAULT_DOCUMENT = 'index.html' @@ -40,6 +41,7 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew // @ts-ignore namespace: options.namespace ? options.namespace : c.env ? c.env.__STATIC_CONTENT : undefined, }) + if (content) { const mimeType = getMimeType(path) if (mimeType) { @@ -47,10 +49,10 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }): Middlew } // Return Response object return c.body(content) - } else { - console.warn(`Static file: ${path} is not found`) - await next() } + + await options.onNotFound?.(path) + await next() return } } diff --git a/src/adapter/deno/serve-static.ts b/src/adapter/deno/serve-static.ts index 7028f9579..ccf9d4b91 100644 --- a/src/adapter/deno/serve-static.ts +++ b/src/adapter/deno/serve-static.ts @@ -11,6 +11,7 @@ export type ServeStaticOptions = { root?: string path?: string rewriteRequestPath?: (path: string) => string + onNotFound?: (path: string) => void | Promise } const DEFAULT_DOCUMENT = 'index.html' @@ -50,10 +51,10 @@ export const serveStatic = (options: ServeStaticOptions = { root: '' }) => { } // Return Response object with stream return c.body(file.readable) - } else { - console.warn(`Static file: ${path} is not found`) - await next() } + + await options.onNotFound?.(path) + await next() return } }