Repository: unjs/h3
Pull request #436 (2023-07-10 15:12:07)

feat: web responses with streaming support
<!---
☝️ 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 directly returning a web [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object from event handlers, leveraging new streaming support for body! (#432)

It is also possible to explicitly use the newly exposed `sendWebResponse` utility.

**Note:** The rest is pretty much simple and implementation is already used for `event.respondWith` (by @danielroe ❤️). I am thinking in next steps, reuse same utility for `respondWith` as well and deprecating pollyfills we initially made due to runtime incompatibilities. (https://github.com/unjs/h3/pull/119) 

Also thanks to @Hebilicious for initial efforts in #395 to push for supporting native Responses ❤️ 

### 📝 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.


pi0: > What happens with the response 2nd argument ?

Status code from `Response` is expected to be picked as it is last operation. Normally we should set it in one place only :) 

> Does it works with Transform Streams?

Readable streams should be normally chainable. I think we can improve tests for `sendStream` utility instead as this feature reuses sendStream. PR Welcome to improve tests 💯 

Patch:
diff --git a/src/app.ts b/src/app.ts
index c06d716..9f46646 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -7,7 +7,14 @@ import {
   H3Event,
 } from "./event";
 import { createError } from "./error";
-import { send, sendStream, isStream, MIMES } from "./utils";
+import {
+  send,
+  sendStream,
+  isStream,
+  MIMES,
+  sendWebResponse,
+  isWebResponse,
+} from "./utils";
 import type { EventHandler, LazyEventHandler } from "./types";
 
 export interface Layer {
@@ -125,6 +132,11 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
       }
 
       if (val) {
+        // Web Response
+        if (isWebResponse(val)) {
+          return sendWebResponse(event, val);
+        }
+
         // Stream
         if (isStream(val)) {
           return sendStream(event, val);
diff --git a/src/utils/response.ts b/src/utils/response.ts
index bf0117f..4494564 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -162,6 +162,10 @@ export function isStream(data: any): data is Readable | ReadableStream {
   return false;
 }
 
+export function isWebResponse(data: any): data is Response {
+  return typeof Response !== "undefined" && data instanceof Response;
+}
+
 export function sendStream(
   event: H3Event,
   stream: Readable | ReadableStream
@@ -270,3 +274,25 @@ export function writeEarlyHints(
     cb();
   }
 }
+
+export function sendWebResponse(event: H3Event, response: Response) {
+  for (const [key, value] of response.headers.entries()) {
+    event.node.res.setHeader(key, value);
+  }
+  if (response.status) {
+    event.node.res.statusCode = sanitizeStatusCode(
+      response.status,
+      event.node.res.statusCode
+    );
+  }
+  if (response.statusText) {
+    event.node.res.statusMessage = sanitizeStatusMessage(response.statusText);
+  }
+  if (response.redirected) {
+    event.node.res.setHeader("location", response.url);
+  }
+  if (!response.body) {
+    return event.node.res.end();
+  }
+  return sendStream(event, response.body);
+}


Test patch:
diff --git a/test/app.test.ts b/test/app.test.ts
index 2886cc5..ef127be 100644
--- a/test/app.test.ts
+++ b/test/app.test.ts
@@ -13,6 +13,7 @@ import {
 // Node.js 16 limitations
 const readableStreamSupported = typeof ReadableStream !== "undefined";
 const blobSupported = typeof Blob !== "undefined";
+const responseSupported = typeof Response !== "undefined";
 
 describe("app", () => {
   let app: App;
@@ -33,6 +34,23 @@ describe("app", () => {
     expect(res.body).toEqual({ url: "/" });
   });
 
+  it.runIf(responseSupported)("can return Response directly", async () => {
+    app.use(
+      "/",
+      eventHandler(
+        () =>
+          new Response("Hello World!", {
+            status: 201,
+            headers: { "x-test": "test" },
+          })
+      )
+    );
+
+    const res = await request.get("/");
+    expect(res.statusCode).toBe(201);
+    expect(res.text).toBe("Hello World!");
+  });
+
   it("can return a 204 response", async () => {
     app.use(
       "/api",