Repository: unjs/h3 (ts)

Issue:
Support optional decoding for `getRouterParam`
### Environment

Node: 18.18.0

### Reproduction

```ts
import { createApp, eventHandler, createRouter } from "h3";

const app = createApp();

const router = createRouter()

router.get(
  '/:name',
  eventHandler((event) => `Hello ${event.context.params.name}!`)
);

app.use(router);
```


### Describe the bug

Expected: **Hello something with test!**
But got: **Hello something%20with%20test!**

param is not being decoded here.

### Additional context

_No response_

### Logs

_No response_

Pull request description:
feat: `decode` option for `getRouterParam`
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org) 
-->

### 🔗 Linked issue

<!-- Please ensure there is an open issue and mention its number as #123 -->

Closes #547.

### ❓ Type of change

<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->

- [ ] 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [x] 👌 Enhancement (improving an existing functionality like performance)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

### 📚 Description

<!-- Describe your changes in detail -->
<!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->

Adding the `decode` option for `getRouterParam` and `getRouterParams`.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] I have linked an issue or discussion.
- [x] I have updated the documentation accordingly.


Patch:
diff --git a/README.md b/README.md
index bbb6386..784f23d 100644
--- a/README.md
+++ b/README.md
@@ -214,9 +214,9 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
 
 - `getQuery(event)`
 - `getValidatedQuery(event, validate)`
-- `getRouterParams(event)`
-- `getValidatedRouterParams(event, validate)`
-- `getRouterParam(event, name)`
+- `getRouterParams(event, { decode? })`
+- `getRouterParam(event, name, { decode? })`
+- `getValidatedRouterParams(event, validate, { decode? })`
 - `getMethod(event, default?)`
 - `isMethod(event, expected, allowHead?)`
 - `assertMethod(event, expected, allowHead?)`
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 28785e8..f97c790 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,4 +1,4 @@
-import { getQuery as _getQuery } from "ufo";
+import { getQuery as _getQuery, decode as decodeURI } from "ufo";
 import { createError } from "../error";
 import type {
   HTTPHeaderName,
@@ -29,17 +29,30 @@ export function getValidatedQuery<
 
 export function getRouterParams(
   event: H3Event,
+  opts: { decode?: boolean } = {},
 ): NonNullable<H3Event["context"]["params"]> {
   // Fallback object needs to be returned in case router is not used (#149)
-  return event.context.params || {};
+  let params = event.context.params || {};
+  if (opts.decode) {
+    params = { ...params };
+    for (const key in params) {
+      params[key] = decodeURI(params[key]);
+    }
+  }
+
+  return params;
 }
 
 export function getValidatedRouterParams<
   T,
   Event extends H3Event = H3Event,
   _T = InferEventInput<"routerParams", Event, T>,
->(event: Event, validate: ValidateFunction<_T>): Promise<_T> {
-  const routerParams = getRouterParams(event);
+>(
+  event: Event,
+  validate: ValidateFunction<_T>,
+  opts: { decode?: boolean } = {},
+): Promise<_T> {
+  const routerParams = getRouterParams(event, opts);
 
   return validateData(routerParams, validate);
 }
@@ -47,8 +60,9 @@ export function getValidatedRouterParams<
 export function getRouterParam(
   event: H3Event,
   name: string,
+  opts: { decode?: boolean } = {},
 ): string | undefined {
-  const params = getRouterParams(event);
+  const params = getRouterParams(event, opts);
 
   return params[name];
 }


Test patch:
diff --git a/test/router.test.ts b/test/router.test.ts
index 6506355..f748b90 100644
--- a/test/router.test.ts
+++ b/test/router.test.ts
@@ -189,6 +189,22 @@ describe("getRouterParams", () => {
 
       expect(result.text).toBe("200");
     });
+
+    it("can decode router params", async () => {
+      const router = createRouter().get(
+        "/test/params/:name",
+        eventHandler((event) => {
+          expect(getRouterParams(event, { decode: true })).toMatchObject({
+            name: "string with space",
+          });
+          return "200";
+        }),
+      );
+      app.use(router);
+      const result = await request.get("/test/params/string with space");
+
+      expect(result.text).toBe("200");
+    });
   });
 
   describe("without router", () => {
@@ -230,6 +246,22 @@ describe("getRouterParam", () => {
 
       expect(result.text).toBe("200");
     });
+
+    it("can decode a value of router params corresponding to the given name", async () => {
+      const router = createRouter().get(
+        "/test/params/:name",
+        eventHandler((event) => {
+          expect(getRouterParam(event, "name", { decode: true })).toEqual(
+            "string with space",
+          );
+          return "200";
+        }),
+      );
+      app.use(router);
+      const result = await request.get("/test/params/string with space");
+
+      expect(result.text).toBe("200");
+    });
   });
 
   describe("without router", () => {