Repository: unjs/h3 (ts)

Issue:
[helpers] Native Request and FormData helpers
### Describe the feature

I've implemented the following in https://github.com/Hebilicious/form-actions-nuxt

```ts
export async function getRequestFromEvent(event: H3Event) {
  const url = new URL(getRequestURL(event))
  const method = getMethod(event)
  const body = method === "POST" ? await readRawBody(event) : undefined
  return new Request(url, { headers: getRequestHeaders(event) as any, method, body })
}

export async function getFormData(event: H3Event) {
  return (await getRequestFromEvent(event)).formData()
}
```

Which directly enables great DX around forms : 

```ts
const eventHandler = event => {
	const formData = await getFormData(event)
	const email = formData.get("email") as string
	const password = formData.get("password") as string
	return { email }
}
```


### Additional information

- [X] Would you be willing to help implement this feature?

Pull request:
feat: add `readFormData` util
Resolves #419 

<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org)

Please carefully read the contribution docs before creating a pull request
 👉 https://nuxt.com/docs/community/contribution
-->

### 🔗 Linked issue

<!-- Please ensure there is an open issue and mention its number as #123 -->

### ❓ Type of change

<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->

- [x] 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [ ] 👌 Enhancement (improving an existing functionality like performance)
- [x] ✨ 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" -->
This PR adds support for Request and FormData in event handler.

2 new helpers are introduced : 

- `getRequestFromEvent`
- `getFormData`

This helpers relies on Request and FormData to be available in the global scope as per the Fetch specification, which means the tests are disabled for node < 18 (which is fine as 18 reaches EOL in 3 months)

```ts
// With a JSON body ...
const handler = eventHandler(async (event) => {
	const nativeRequest = await getRequestFromEvent(event);
	expect(nativeRequest instanceof Request).toBe(true);
	expect(nativeRequest.method).toBe("POST");
	expect(nativeRequest.headers.get("hello")).toBe("world");
	const body = await nativeRequest.json();
	expect(body).toMatchObject({
	  user: "john",
	});
	return "ok";
})
```  
        
```ts
// With x-www-form-urlencoded data
const handler = eventHandler(async (event) => {
	const formData = await getFormData(event);
	const user = formData.get("user");
	expect(formData instanceof FormData).toBe(true);
	expect(user).toBe("john");
	return { user };
})
``` 

### 📝 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/src/utils/body.ts b/src/utils/body.ts
index c53b90a..cb63eae 100644
--- a/src/utils/body.ts
+++ b/src/utils/body.ts
@@ -134,6 +134,23 @@ export async function readMultipartFormData(event: H3Event) {
   return parseMultipartData(body, boundary);
 }
 
+/**
+ * Constructs a FormData object from an event.
+ * @param event {H3Event}
+ * @returns {FormData}
+ *
+ * ```ts
+ * const eventHandler = event => {
+ *   const formData = await readFormData(event)
+ *   const email = formData.get("email")
+ *   const password = formData.get("password")
+ *  }
+ * ```
+ */
+export async function readFormData(event: H3Event) {
+  return await event.request.formData();
+}
+
 // --- Internal ---
 
 function _parseJSON(body = "", strict: boolean) {


Test patch:
diff --git a/test/utils.test.ts b/test/utils.test.ts
index 398259c..31e7e9a 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -11,6 +11,7 @@ import {
   getMethod,
   getQuery,
   getRequestURL,
+  readFormData,
 } from "../src";
 
 describe("", () => {
@@ -158,4 +159,28 @@ describe("", () => {
       expect((await request.head("/post")).status).toBe(200);
     });
   });
+
+  const below18 = Number.parseInt(process.version.slice(1).split(".")[0]) < 18;
+  describe.skipIf(below18)("readFormData", () => {
+    it("can handle form as FormData in event handler", async () => {
+      app.use(
+        "/",
+        eventHandler(async (event) => {
+          const formData = await readFormData(event);
+          const user = formData.get("user");
+          expect(formData instanceof FormData).toBe(true);
+          expect(user).toBe("john");
+          return { user };
+        })
+      );
+
+      const result = await request
+        .post("/api/test")
+        .set("content-type", "application/x-www-form-urlencoded")
+        .field("user", "john");
+
+      expect(result.status).toBe(200);
+      expect(result.body).toMatchObject({ user: "john" });
+    });
+  });
 });