Repository: honojs/hono (TypeScript)

Issue:
Param value of "keys" resolves to a function, not the value "keys"
### What version of Hono are you using?

3.10.1

### What runtime/platform is your app running on?

Node, Cloudflare Workers

### What steps can reproduce the bug?

Create a Hono app with multiple nested routes:

```ts
// Adapted from Hono's own hono.test.ts, 'Routing' -> 'Nested route' test:

const book = app.basePath("/book");
book.get("/:id", c => {
	// PROBLEM HERE:
	return c.text(`get /book/${c.req.param("id")}`);
});

const user = app.basePath("/user");
user.get("/login", c => c.text("get /user/login"));

const appForEachUser = user.basePath(":id");
appForEachUser.get("/profile", c => c.text(`get /user/${c.req.param("id")}/profile`));
```

### What is the expected behavior?

When a caller requests a route where the `:id` param value is `"keys"`, the result of `c.req.param("id")` should be `"keys"`.

### What do you see instead?

`c.req.param("id")` returns a function instead.

### Additional information

The function seems to be `Array.prototype.keys`. I'm not sure yet under exactly which circumstances this happens, but at some point, when constructing the request context, the params get constructed such that Hono tries to access the `"keys"` property of an array, rather than of an object, and finding a value (the `keys` method of the array), returns it instead of the expected string value.

I have a minimal reproduction of this issue at https://github.com/AverageHelper/hono-bug-param-keys

A failing test for this test may be added to Hono at [src/hono.test.ts](https://github.com/honojs/hono/blob/1ed742bdb80518437e6d6f9c6ec7a5fbe300f254/src/hono.test.ts#L296), around line 296, like so:
```ts
// ...

    res = await app.request('http://localhost/book/123', { method: 'GET' })
    expect(res.status).toBe(200)
    expect(await res.text()).toBe('get /book/123') // currently works

    res = await app.request('http://localhost/book/keys', { method: 'GET' })
    expect(res.status).toBe(200)
    expect(await res.text()).toBe('get /book/keys') // currently fails, as described

// ...
```


Discussion: I have [a fork](https://github.com/AverageHelper/hono/tree/avg/param-keys) where I've already added a failing test as described.

The issue seems primarily to occur in src/request.ts:56: https://github.com/honojs/hono/blob/1ed742bdb80518437e6d6f9c6ec7a5fbe300f254/src/request.ts#L56

In the failing case, `this._s` is an empty array, `this._p` is an object with the shape `{ id: 'keys' }`, and `key` is `'id'`. `this._p[key]` results in the value `'keys'`, which would be the expected correct return value in this case, but Hono continues with `this._s['keys']`, which of course results in [`Array.prototype.keys`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/keys).

I don't know enough yet about Hono internals to get more concrete details than this, much less work out a proper fix.
Hi @AverageHelper!

Thanks for the bug report. I appreciate it.
We will fix it.
@usualoma Thanks for handling!




Fix (patch):
diff --git a/deno_dist/hono-base.ts b/deno_dist/hono-base.ts
index 6a356d960..5cfc267c0 100644
--- a/deno_dist/hono-base.ts
+++ b/deno_dist/hono-base.ts
@@ -279,7 +279,7 @@ class Hono<
     const path = this.getPath(request, { env })
     const [handlers, paramStash] = this.matchRoute(method, path)
 
-    const c = new Context(new HonoRequest(request, path, paramStash || []), {
+    const c = new Context(new HonoRequest(request, path, paramStash), {
       env,
       executionCtx,
       notFoundHandler: this.notFoundHandler,
diff --git a/deno_dist/request.ts b/deno_dist/request.ts
index 58dc8f778..c169a9661 100644
--- a/deno_dist/request.ts
+++ b/deno_dist/request.ts
@@ -29,13 +29,17 @@ type BodyCache = Partial<Body & { parsedBody: BodyData }>
 export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
   raw: Request
 
-  private _s: ParamStash
+  private _s: ParamStash | undefined
   private vData: { [K in keyof ValidationTargets]?: {} } // Short name of validatedData
   private _p: ParamIndexMap | Params = {}
   path: string
   bodyCache: BodyCache = {}
 
-  constructor(request: Request, path: string = '/', paramStash: ParamStash = []) {
+  constructor(
+    request: Request,
+    path: string = '/',
+    paramStash: ParamStash | undefined = undefined
+  ) {
     this.raw = request
     this.path = path
     this._s = paramStash
@@ -51,26 +55,23 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
   ): UndefinedIfHavingQuestion<ParamKeys<P2>>
   param<P2 extends string = P>(): UnionToIntersection<ParamKeyToRecord<ParamKeys<P2>>>
   param(key?: string): unknown {
-    if (this._s) {
-      if (key) {
-        const param = this._s[this._p[key] as any] ?? this._p[key]
-        return param ? (/\%/.test(param) ? decodeURIComponent_(param) : param) : undefined
-      } else {
-        const decoded: Record<string, string> = {}
-
-        const keys = Object.keys(this._p)
-        for (let i = 0, len = keys.length; i < len; i++) {
-          const key = keys[i]
-          const value = this._s[this._p[key] as any] ?? this._p[key]
-          if (value && typeof value === 'string') {
-            decoded[key] = /\%/.test(value) ? decodeURIComponent_(value) : value
-          }
+    if (key) {
+      const param = (this._s ? this._s[this._p[key] as any] : this._p[key]) as string | undefined
+      return param ? (/\%/.test(param) ? decodeURIComponent_(param) : param) : undefined
+    } else {
+      const decoded: Record<string, string> = {}
+
+      const keys = Object.keys(this._p)
+      for (let i = 0, len = keys.length; i < len; i++) {
+        const key = keys[i]
+        const value = (this._s ? this._s[this._p[key] as any] : this._p[key]) as string | undefined
+        if (value && typeof value === 'string') {
+          decoded[key] = /\%/.test(value) ? decodeURIComponent_(value) : value
         }
-
-        return decoded
       }
+
+      return decoded
     }
-    return null
   }
 
   query(key: string): string | undefined
diff --git a/src/hono-base.ts b/src/hono-base.ts
index 60781609f..64ab3e926 100644
--- a/src/hono-base.ts
+++ b/src/hono-base.ts
@@ -279,7 +279,7 @@ class Hono<
     const path = this.getPath(request, { env })
     const [handlers, paramStash] = this.matchRoute(method, path)
 
-    const c = new Context(new HonoRequest(request, path, paramStash || []), {
+    const c = new Context(new HonoRequest(request, path, paramStash), {
       env,
       executionCtx,
       notFoundHandler: this.notFoundHandler,
diff --git a/src/request.ts b/src/request.ts
index 69bc3084d..813487b8b 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -29,13 +29,17 @@ type BodyCache = Partial<Body & { parsedBody: BodyData }>
 export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
   raw: Request
 
-  private _s: ParamStash
+  private _s: ParamStash | undefined
   private vData: { [K in keyof ValidationTargets]?: {} } // Short name of validatedData
   private _p: ParamIndexMap | Params = {}
   path: string
   bodyCache: BodyCache = {}
 
-  constructor(request: Request, path: string = '/', paramStash: ParamStash = []) {
+  constructor(
+    request: Request,
+    path: string = '/',
+    paramStash: ParamStash | undefined = undefined
+  ) {
     this.raw = request
     this.path = path
     this._s = paramStash
@@ -51,26 +55,23 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
   ): UndefinedIfHavingQuestion<ParamKeys<P2>>
   param<P2 extends string = P>(): UnionToIntersection<ParamKeyToRecord<ParamKeys<P2>>>
   param(key?: string): unknown {
-    if (this._s) {
-      if (key) {
-        const param = this._s[this._p[key] as any] ?? this._p[key]
-        return param ? (/\%/.test(param) ? decodeURIComponent_(param) : param) : undefined
-      } else {
-        const decoded: Record<string, string> = {}
-
-        const keys = Object.keys(this._p)
-        for (let i = 0, len = keys.length; i < len; i++) {
-          const key = keys[i]
-          const value = this._s[this._p[key] as any] ?? this._p[key]
-          if (value && typeof value === 'string') {
-            decoded[key] = /\%/.test(value) ? decodeURIComponent_(value) : value
-          }
+    if (key) {
+      const param = (this._s ? this._s[this._p[key] as any] : this._p[key]) as string | undefined
+      return param ? (/\%/.test(param) ? decodeURIComponent_(param) : param) : undefined
+    } else {
+      const decoded: Record<string, string> = {}
+
+      const keys = Object.keys(this._p)
+      for (let i = 0, len = keys.length; i < len; i++) {
+        const key = keys[i]
+        const value = (this._s ? this._s[this._p[key] as any] : this._p[key]) as string | undefined
+        if (value && typeof value === 'string') {
+          decoded[key] = /\%/.test(value) ? decodeURIComponent_(value) : value
         }
-
-        return decoded
       }
+
+      return decoded
     }
-    return null
   }
 
   query(key: string): string | undefined