Repository: unjs/h3
Pull request #454 (2023-07-24 16:33:54)

feat(event): `event.request` getter to access web request
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org) 
-->

### 🔗 Linked issue

#73 

### ❓ 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)
- [ ] 👌 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

This PR adds support for the lazy initialized `event.request` property that resolves to a standard [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request).

**Note:** It is still **highly recommended** to prefer using top level event API (`event.{path,method,headers}`) as it has lower foot print and can be optimized per runtime platform.

Known issues: Node.js 16 does not supports ReadableStream as body for Request constructor

### 📝 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! -->

- [ ] I have linked an issue or discussion.
- [ ] I have updated the documentation accordingly.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/unjs/h3/pull/454?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs) Report
> Merging [#454](https://app.codecov.io/gh/unjs/h3/pull/454?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs) (a8c91c5) into [main](https://app.codecov.io/gh/unjs/h3/commit/a9b8de8a3e1eac9d9db513021e369b35dfdeb5fd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs) (a9b8de8) will **decrease** coverage by `1.56%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #454      +/-   ##
==========================================
- Coverage   77.93%   76.37%   -1.56%     
==========================================
  Files          26       26              
  Lines        2724     2739      +15     
  Branches      399      397       -2     
==========================================
- Hits         2123     2092      -31     
- Misses        601      647      +46     
```


| [Impacted Files](https://app.codecov.io/gh/unjs/h3/pull/454?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs) | Coverage Δ | |
|---|---|---|
| [src/event/event.ts](https://app.codecov.io/gh/unjs/h3/pull/454?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs#diff-c3JjL2V2ZW50L2V2ZW50LnRz) | `70.85% <100.00%> (+2.73%)` | :arrow_up: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/unjs/h3/pull/454/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unjs)



Patch:
diff --git a/src/event/event.ts b/src/event/event.ts
index 8dbf406..58240e5 100644
--- a/src/event/event.ts
+++ b/src/event/event.ts
@@ -9,7 +9,16 @@ import {
 } from "../utils";
 import { H3Response } from "./response";
 
-const DOUBLE_SLASH_RE = /[/\\]{2,}/g; // TODO: Dedup from request.ts
+// TODO: Dedup from request.ts
+const DOUBLE_SLASH_RE = /[/\\]{2,}/g;
+
+// TODO: Dedup from body.ts
+const PayloadMethods: Set<HTTPMethod> = new Set([
+  "PATCH",
+  "POST",
+  "PUT",
+  "DELETE",
+]);
 
 export interface NodeEventContext {
   req: NodeIncomingMessage;
@@ -24,6 +33,7 @@ export class H3Event implements Pick<FetchEvent, "respondWith"> {
   context: H3EventContext = {};
 
   // Request
+  _request: Request | undefined;
   _method: HTTPMethod | undefined;
   _headers: Headers | undefined;
   _path: string | undefined;
@@ -45,6 +55,10 @@ export class H3Event implements Pick<FetchEvent, "respondWith"> {
     );
   }
 
+  get _hasBody() {
+    return PayloadMethods.has(this.method!);
+  }
+
   get path() {
     if (!this._path) {
       this._path = this._originalPath.replace(DOUBLE_SLASH_RE, "/");
@@ -87,7 +101,10 @@ export class H3Event implements Pick<FetchEvent, "respondWith"> {
   }
 
   get body() {
-    if (!this._body) {
+    if (!this._hasBody) {
+      return undefined;
+    }
+    if (this._body === undefined) {
       this._body = new ReadableStream({
         start: (controller) => {
           this.node.req.on("data", (chunk) => {
@@ -105,6 +122,20 @@ export class H3Event implements Pick<FetchEvent, "respondWith"> {
     return this._body;
   }
 
+  /** @experimental */
+  get request(): Request {
+    if (!this._request) {
+      this._request = new Request(this.url, {
+        // @ts-ignore Undici option
+        duplex: "half",
+        method: this.method,
+        headers: this.headers,
+        body: this.body,
+      });
+    }
+    return this._request;
+  }
+
   // Implementation of FetchEvent
   respondWith(r: H3Response | PromiseLike<H3Response>): void {
     Promise.resolve(r).then((_response) => {


Test patch:
diff --git a/test/event.test.ts b/test/event.test.ts
index 4eb6d32..a98731d 100644
--- a/test/event.test.ts
+++ b/test/event.test.ts
@@ -78,4 +78,28 @@ describe("Event", () => {
 
     expect(result.body).toMatchObject({ bytes: 3 });
   });
+
+  it("can convert to a web request", async () => {
+    app.use(
+      "/",
+      eventHandler(async (event) => {
+        expect(event.request.method).toBe("POST");
+        expect(event.request.headers.get("x-test")).toBe("123");
+        // TODO: Find a workaround for Node.js 16
+        if (!process.versions.node.startsWith("16")) {
+          expect(await event.request.text()).toMatchObject(
+            JSON.stringify({ hello: "world" })
+          );
+        }
+        return "200";
+      })
+    );
+    const result = await request
+      .post("/hello")
+      .set("x-test", "123")
+      .set("content-type", "application/json")
+      .send(JSON.stringify({ hello: "world" }));
+
+    expect(result.text).toBe("200");
+  });
 });