diff --git a/package.json b/package.json index 669c20277..aa2e0a6c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hono", - "version": "4.6.13", + "version": "4.6.14", "description": "Web framework built on Web Standards", "main": "dist/cjs/index.js", "type": "module", diff --git a/runtime-tests/lambda/index.test.ts b/runtime-tests/lambda/index.test.ts index e5b06de19..adf9fc835 100644 --- a/runtime-tests/lambda/index.test.ts +++ b/runtime-tests/lambda/index.test.ts @@ -537,7 +537,7 @@ describe('AWS Lambda Adapter for Hono', () => { expect(albResponse.statusCode).toBe(200) expect(albResponse.headers).toEqual( expect.objectContaining({ - 'content-type': 'application/json; charset=UTF-8', + 'content-type': 'application/json', }) ) }) @@ -598,7 +598,7 @@ describe('AWS Lambda Adapter for Hono', () => { expect(albResponse.multiValueHeaders).toBeDefined() expect(albResponse.multiValueHeaders).toEqual( expect.objectContaining({ - 'content-type': ['application/json; charset=UTF-8'], + 'content-type': ['application/json'], }) ) }) diff --git a/src/adapter/aws-lambda/handler.test.ts b/src/adapter/aws-lambda/handler.test.ts index 1d4358aec..97c7da4c1 100644 --- a/src/adapter/aws-lambda/handler.test.ts +++ b/src/adapter/aws-lambda/handler.test.ts @@ -13,7 +13,7 @@ describe('isContentTypeBinary', () => { expect(isContentTypeBinary('text/javascript')).toBe(false) expect(isContentTypeBinary('application/json')).toBe(false) expect(isContentTypeBinary('application/ld+json')).toBe(false) - expect(isContentTypeBinary('application/json; charset=UTF-8')).toBe(false) + expect(isContentTypeBinary('application/json')).toBe(false) }) }) diff --git a/src/adapter/lambda-edge/handler.test.ts b/src/adapter/lambda-edge/handler.test.ts index aa17c904a..d9bbba863 100644 --- a/src/adapter/lambda-edge/handler.test.ts +++ b/src/adapter/lambda-edge/handler.test.ts @@ -13,7 +13,7 @@ describe('isContentTypeBinary', () => { expect(isContentTypeBinary('text/javascript')).toBe(false) expect(isContentTypeBinary('application/json')).toBe(false) expect(isContentTypeBinary('application/ld+json')).toBe(false) - expect(isContentTypeBinary('application/json; charset=UTF-8')).toBe(false) + expect(isContentTypeBinary('application/json')).toBe(false) }) }) diff --git a/src/context.test.ts b/src/context.test.ts index 622852604..8230a9548 100644 --- a/src/context.test.ts +++ b/src/context.test.ts @@ -51,7 +51,7 @@ describe('Context', () => { it('c.json()', async () => { const res = c.json({ message: 'Hello' }, 201, { 'X-Custom': 'Message' }) expect(res.status).toBe(201) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') const text = await res.text() expect(text).toBe('{"message":"Hello"}') expect(res.headers.get('X-Custom')).toBe('Message') @@ -182,7 +182,7 @@ describe('Context', () => { c.status(404) const res = c.json({ hono: 'great app' }) expect(res.status).toBe(404) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') const obj: { [key: string]: string } = await res.json() expect(obj['hono']).toBe('great app') }) diff --git a/src/context.ts b/src/context.ts index 86124e008..8447f2128 100644 --- a/src/context.ts +++ b/src/context.ts @@ -777,7 +777,7 @@ export class Context< ): JSONRespondReturn => { const body = JSON.stringify(object) this.#preparedHeaders ??= {} - this.#preparedHeaders['content-type'] = 'application/json; charset=UTF-8' + this.#preparedHeaders['content-type'] = 'application/json' /* eslint-disable @typescript-eslint/no-explicit-any */ return ( typeof arg === 'number' ? this.#newResponse(body, arg, headers) : this.#newResponse(body, arg) diff --git a/src/helper/streaming/stream.test.ts b/src/helper/streaming/stream.test.ts index 820579de5..4a9eff2ed 100644 --- a/src/helper/streaming/stream.test.ts +++ b/src/helper/streaming/stream.test.ts @@ -71,6 +71,28 @@ describe('Basic Streaming Helper', () => { expect(aborted).toBeTruthy() }) + it('Check stream Response if pipe is aborted by abort signal', async () => { + const ac = new AbortController() + const req = new Request('http://localhost/', { signal: ac.signal }) + const c = new Context(req) + + let aborted = false + const res = stream(c, async (stream) => { + stream.onAbort(() => { + aborted = true + }) + await stream.pipe(new ReadableStream()) + }) + if (!res.body) { + throw new Error('Body is null') + } + const reader = res.body.getReader() + const pReading = reader.read() + ac.abort() + await pReading + expect(aborted).toBeTruthy() + }) + it('Check stream Response if error occurred', async () => { const onError = vi.fn() const res = stream( diff --git a/src/helper/streaming/stream.ts b/src/helper/streaming/stream.ts index 3d0449573..5878c2188 100644 --- a/src/helper/streaming/stream.ts +++ b/src/helper/streaming/stream.ts @@ -22,7 +22,11 @@ export const stream = ( try { await cb(stream) } catch (e) { - if (e instanceof Error && onError) { + if (e === undefined) { + // If reading is canceled without a reason value (e.g. by StreamingApi) + // then the .pipeTo() promise will reject with undefined. + // In this case, do nothing because the stream is already closed. + } else if (e instanceof Error && onError) { await onError(e, stream) } else { console.error(e) diff --git a/src/middleware/basic-auth/index.test.ts b/src/middleware/basic-auth/index.test.ts index e64d2db59..c4065e60e 100644 --- a/src/middleware/basic-auth/index.test.ts +++ b/src/middleware/basic-auth/index.test.ts @@ -295,7 +295,7 @@ describe('Basic Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe('{"message":"Custom unauthorized message as object"}') }) @@ -314,7 +314,7 @@ describe('Basic Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe('{"message":"Custom unauthorized message as function object"}') }) diff --git a/src/middleware/basic-auth/index.ts b/src/middleware/basic-auth/index.ts index aa4ca72d0..45d5f6c65 100644 --- a/src/middleware/basic-auth/index.ts +++ b/src/middleware/basic-auth/index.ts @@ -120,7 +120,7 @@ export const basicAuth = ( status, headers: { ...headers, - 'content-type': 'application/json; charset=UTF-8', + 'content-type': 'application/json', }, }) throw new HTTPException(status, { res }) diff --git a/src/middleware/bearer-auth/index.test.ts b/src/middleware/bearer-auth/index.test.ts index 4dca87f22..2b559c021 100644 --- a/src/middleware/bearer-auth/index.test.ts +++ b/src/middleware/bearer-auth/index.test.ts @@ -400,7 +400,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe('{"message":"Custom no authentication header message as object"}') }) @@ -423,7 +423,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe( '{"message":"Custom no authentication header message as function object"}' @@ -450,7 +450,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(400) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe( '{"message":"Custom invalid authentication header message as object"}' @@ -477,7 +477,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(400) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe( '{"message":"Custom invalid authentication header message as function object"}' @@ -500,7 +500,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe('{"message":"Custom invalid token message as object"}') }) @@ -521,7 +521,7 @@ describe('Bearer Auth by Middleware', () => { const res = await app.request(req) expect(res).not.toBeNull() expect(res.status).toBe(401) - expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(res.headers.get('Content-Type')).toMatch('application/json') expect(handlerExecuted).toBeFalsy() expect(await res.text()).toBe('{"message":"Custom invalid token message as function object"}') }) diff --git a/src/middleware/bearer-auth/index.ts b/src/middleware/bearer-auth/index.ts index 0e86d4d47..7a83345f4 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -103,7 +103,7 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { status, headers: { ...headers, - 'content-type': 'application/json; charset=UTF-8', + 'content-type': 'application/json', }, }) throw new HTTPException(status, { res }) diff --git a/src/router/pattern-router/router.ts b/src/router/pattern-router/router.ts index d15610409..5f599471e 100644 --- a/src/router/pattern-router/router.ts +++ b/src/router/pattern-router/router.ts @@ -3,6 +3,8 @@ import { METHOD_NAME_ALL, UnsupportedPathError } from '../../router' type Route = [RegExp, string, T] // [pattern, method, handler, path] +const emptyParams = Object.create(null) + export class PatternRouter implements Router { name: string = 'PatternRouter' #routes: Route[] = [] @@ -46,7 +48,7 @@ export class PatternRouter implements Router { if (routeMethod === method || routeMethod === METHOD_NAME_ALL) { const match = pattern.exec(path) if (match) { - handlers.push([handler, match.groups || Object.create(null)]) + handlers.push([handler, match.groups || emptyParams]) } } } diff --git a/src/router/trie-router/node.ts b/src/router/trie-router/node.ts index 94ab4de19..b19f8cff3 100644 --- a/src/router/trie-router/node.ts +++ b/src/router/trie-router/node.ts @@ -13,13 +13,15 @@ type HandlerParamsSet = HandlerSet & { params: Record } +const emptyParams = Object.create(null) + export class Node { #methods: Record>[] #children: Record> #patterns: Pattern[] #order: number = 0 - #params: Record = Object.create(null) + #params: Record = emptyParams constructor(method?: string, handler?: T, children?: Record>) { this.#children = children || Object.create(null) @@ -82,7 +84,7 @@ export class Node { node: Node, method: string, nodeParams: Record, - params: Record + params?: Record ): HandlerParamsSet[] { const handlerSets: HandlerParamsSet[] = [] for (let i = 0, len = node.#methods.length; i < len; i++) { @@ -91,15 +93,16 @@ export class Node { const processedSet: Record = {} if (handlerSet !== undefined) { handlerSet.params = Object.create(null) - for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) { - const key = handlerSet.possibleKeys[i] - const processed = processedSet[handlerSet.score] - handlerSet.params[key] = - params[key] && !processed ? params[key] : nodeParams[key] ?? params[key] - processedSet[handlerSet.score] = true - } - handlerSets.push(handlerSet) + if (nodeParams !== emptyParams || (params && params !== emptyParams)) { + for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) { + const key = handlerSet.possibleKeys[i] + const processed = processedSet[handlerSet.score] + handlerSet.params[key] = + params?.[key] && !processed ? params[key] : nodeParams[key] ?? params?.[key] + processedSet[handlerSet.score] = true + } + } } } return handlerSets @@ -107,7 +110,7 @@ export class Node { search(method: string, path: string): [[T, Params][]] { const handlerSets: HandlerParamsSet[] = [] - this.#params = Object.create(null) + this.#params = emptyParams // eslint-disable-next-line @typescript-eslint/no-this-alias const curNode: Node = this @@ -129,17 +132,10 @@ export class Node { // '/hello/*' => match '/hello' if (nextNode.#children['*']) { handlerSets.push( - ...this.#getHandlerSets( - nextNode.#children['*'], - method, - node.#params, - Object.create(null) - ) + ...this.#getHandlerSets(nextNode.#children['*'], method, node.#params) ) } - handlerSets.push( - ...this.#getHandlerSets(nextNode, method, node.#params, Object.create(null)) - ) + handlerSets.push(...this.#getHandlerSets(nextNode, method, node.#params)) } else { tempNodes.push(nextNode) } @@ -147,17 +143,14 @@ export class Node { for (let k = 0, len3 = node.#patterns.length; k < len3; k++) { const pattern = node.#patterns[k] - - const params = { ...node.#params } + const params = node.#params === emptyParams ? {} : { ...node.#params } // Wildcard // '/hello/*/foo' => match /hello/bar/foo if (pattern === '*') { const astNode = node.#children['*'] if (astNode) { - handlerSets.push( - ...this.#getHandlerSets(astNode, method, node.#params, Object.create(null)) - ) + handlerSets.push(...this.#getHandlerSets(astNode, method, node.#params)) tempNodes.push(astNode) } continue diff --git a/src/utils/mime.test.ts b/src/utils/mime.test.ts index 70961c813..f89fbc06b 100644 --- a/src/utils/mime.test.ts +++ b/src/utils/mime.test.ts @@ -9,7 +9,7 @@ describe('mime', () => { it('getMimeType', () => { expect(getMimeType('hello.txt')).toBe('text/plain; charset=utf-8') expect(getMimeType('hello.html')).toBe('text/html; charset=utf-8') - expect(getMimeType('hello.json')).toBe('application/json; charset=utf-8') + expect(getMimeType('hello.json')).toBe('application/json') expect(getMimeType('favicon.ico')).toBe('image/x-icon') expect(getMimeType('good.morning.hello.gif')).toBe('image/gif') expect(getMimeType('goodmorninghellogif')).toBeUndefined() diff --git a/src/utils/mime.ts b/src/utils/mime.ts index 40f250af3..01aa08dc9 100644 --- a/src/utils/mime.ts +++ b/src/utils/mime.ts @@ -13,7 +13,7 @@ export const getMimeType = ( return } let mimeType = mimes[match[1]] - if ((mimeType && mimeType.startsWith('text')) || mimeType === 'application/json') { + if (mimeType && mimeType.startsWith('text')) { mimeType += '; charset=utf-8' } return mimeType diff --git a/src/validator/validator.test.ts b/src/validator/validator.test.ts index 83af4481a..05ff812fd 100644 --- a/src/validator/validator.test.ts +++ b/src/validator/validator.test.ts @@ -140,7 +140,7 @@ describe('JSON', () => { const res = await app.request('http://localhost/post', { method: 'POST', headers: { - 'Content-Type': 'application/json; charset=utf8', + 'Content-Type': 'application/json', }, body: JSON.stringify({ foo: 'bar' }), })