hono icon indicating copy to clipboard operation
hono copied to clipboard

`Content-Type` header only works with `ctx.body()`

Open LoryPelli opened this issue 11 months ago • 10 comments

What version of Hono are you using?

4.7.0

What runtime/platform is your app running on? (with version if possible)

Cloudflare Workers

What steps can reproduce the bug?

The following code DOESN'T work

ctx.text(await res.text(), {
        headers: {
            'Content-Type': 'text/javascript',
        },
});

The following code DOES work

ctx.body(await res.text(), {
        headers: {
            'Content-Type': 'text/javascript',
        },
});

You should make text function (but probably also json and others) to omit Content-Type header from his typescript type...

What is the expected behavior?

if it doesn't work typescript should complain (also javascript because you shouldn't be able to do a thing like this at all)

What do you see instead?

no error but wrong Content-Type (text/plain in my case)

Additional information

No response

LoryPelli avatar Feb 09 '25 09:02 LoryPelli

The internal header seems to take precedence.

const setHeaders = (headers: Headers, map: Record<string, string> = {}) => {
  for (const key of Object.keys(map)) {
    headers.set(key, map[key])
  }
  return headers
}

EdamAme-x avatar Feb 09 '25 09:02 EdamAme-x

The internal header seems to take precedence.

const setHeaders = (headers: Headers, map: Record<string, string> = {}) => {
  for (const key of Object.keys(map)) {
    headers.set(key, map[key])
  }
  return headers
}

I guess this is intended behaviour because ctx.text() is just normal text, you shouldn't be able to override Content-Type and in fact it doesn't work...

LoryPelli avatar Feb 09 '25 10:02 LoryPelli

Just that a sintax like that should throw an error

LoryPelli avatar Feb 09 '25 10:02 LoryPelli

hmm... Whether the error is indicated by the typescript or detected internally, the cost of detector is too high.

I think we need to hope that this issue will make users aware of this.

EdamAme-x avatar Feb 09 '25 10:02 EdamAme-x

hmm... Whether the error is indicated by the typescript or detected internally, the cost of detector is too high.

I think we need to hope that this issue will make users aware of this.

isn't just Omit<Headers, "Content-Type"> for typescript and if (headers.includes("Content-Type") for javascript?

LoryPelli avatar Feb 09 '25 10:02 LoryPelli

Like this? If you use c.text(),, the content type will be inferred as only text/plain. But I can't find a good way to throw a type error now. And type definitions will be complex.

Image

yusukebe avatar Feb 09 '25 10:02 yusukebe

I don't think it is necessary to add this to the internal implementation, as it can be avoided if the user is careful.

Types are something to consider, but Hono's internal types are flapped several times, which complicates the code a bit.

EdamAme-x avatar Feb 09 '25 10:02 EdamAme-x

This is the diff (but verbose):

diff --git a/src/context.ts b/src/context.ts
index 9c19eff3..50ed2d06 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -21,10 +21,9 @@ import type {
   SimplifyDeepArray,
 } from './utils/types'
 
-type HeaderRecord =
-  | Record<'Content-Type', BaseMime>
-  | Record<ResponseHeader, string | string[]>
-  | Record<string, string | string[]>
+type HeaderRecord<
+  ContentType extends Record<'Content-Type', string> = Record<'Content-Type', BaseMime>
+> = ContentType | Record<ResponseHeader, string | string[]> | Record<string, string | string[]>
 
 /**
  * Data type can be a string, ArrayBuffer, Uint8Array (buffer), or ReadableStream.
@@ -139,15 +138,17 @@ interface BodyRespond {
  *
  * @returns {Response & TypedResponse<T, U, 'text'>} - The response after rendering the text content, typed with the provided text and status code types.
  */
-interface TextRespond {
+interface TextRespond<
+  ContentType extends Record<'Content-Type', string> = Record<'Content-Type', BaseMime>
+> {
   <T extends string, U extends ContentfulStatusCode = ContentfulStatusCode>(
     text: T,
     status?: U,
-    headers?: HeaderRecord
+    headers?: HeaderRecord<ContentType>
   ): Response & TypedResponse<T, U, 'text'>
   <T extends string, U extends ContentfulStatusCode = ContentfulStatusCode>(
     text: T,
-    init?: ResponseOrInit<U>
+    init?: ResponseOrInit<U, ContentType>
   ): Response & TypedResponse<T, U, 'text'>
 }
 
@@ -256,20 +257,28 @@ interface SetHeaders {
   (name: string, value?: string, options?: SetHeadersOptions): void
 }
 
-type ResponseHeadersInit =
+type ResponseHeadersInit<
+  ContentType extends Record<'Content-Type', string> = Record<'Content-Type', BaseMime>
+> =
   | [string, string][]
-  | Record<'Content-Type', BaseMime>
+  | ContentType
   | Record<ResponseHeader, string>
   | Record<string, string>
   | Headers
 
-interface ResponseInit<T extends StatusCode = StatusCode> {
-  headers?: ResponseHeadersInit
+interface ResponseInit<
+  T extends StatusCode = StatusCode,
+  ContentType extends Record<'Content-Type', string> = Record<'Content-Type', BaseMime>
+> {
+  headers?: ResponseHeadersInit<ContentType>
   status?: T
   statusText?: string
 }
 
-type ResponseOrInit<T extends StatusCode = StatusCode> = ResponseInit<T> | Response
+type ResponseOrInit<
+  T extends StatusCode = StatusCode,
+  ContentType extends Record<'Content-Type', string> = Record<'Content-Type', BaseMime>
+> = ResponseInit<T, ContentType> | Response
 
 export const TEXT_PLAIN = 'text/plain; charset=UTF-8'
 
@@ -749,7 +758,7 @@ export class Context<
    * })
    * ```
    */
-  text: TextRespond = (
+  text: TextRespond<Record<'Content-Type', 'text/plain'>> = (
     text: string,
     arg?: ContentfulStatusCode | ResponseOrInit,
     headers?: HeaderRecord

yusukebe avatar Feb 09 '25 10:02 yusukebe

The types are even more complex... I wonder the inference time.

EdamAme-x avatar Feb 09 '25 10:02 EdamAme-x

This is closer to a feature request than a bug. Therefore, I will change the label.

yusukebe avatar May 20 '25 22:05 yusukebe