Skip to content

Commit

Permalink
refactor!: overhaul app event handler (#792)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Jun 26, 2024
1 parent 16b2c7d commit 396aa4b
Show file tree
Hide file tree
Showing 35 changed files with 654 additions and 685 deletions.
52 changes: 18 additions & 34 deletions docs/2.utils/2.response.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,7 @@ export default defineEventHandler((event) => {
});
```

### `removeResponseHeader(event, name)`

Remove a response header by name.

**Example:**

```ts
export default defineEventHandler((event) => {
removeResponseHeader(event, "content-type"); // Remove content-type header
});
```

### `sendIterable(event, iterable)`
### `iterable(iterable)`

Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks.

Expand All @@ -114,8 +102,7 @@ For generator (yielding) functions, the returned value is treated the same as yi
**Example:**

```ts
sendIterable(event, work());
async function* work() {
return iterable(async function* work() {
// Open document body
yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
// Do work ...
Expand All @@ -128,36 +115,25 @@ async function* work() {
}
// Close out the report
return `</ol></body></html>`;
}
})
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
```

### `sendNoContent(event, code?)`
### `noContent(event, code?)`

Respond with an empty payload.<br>

Note that calling this function will close the connection and no other data can be sent to the client afterwards.

**Example:**

```ts
export default defineEventHandler((event) => {
return sendNoContent(event);
return noContent(event);
});
```

**Example:**

```ts
export default defineEventHandler((event) => {
sendNoContent(event); // Close the connection
console.log("This will not be executed");
});
```

### `sendRedirect(event, location, code)`
### `redirect(event, location, code)`

Send a redirect response to the client.

Expand All @@ -169,21 +145,29 @@ In the body, it sends a simple HTML page with a meta refresh tag to redirect the

```ts
export default defineEventHandler((event) => {
return sendRedirect(event, "https://example.com");
return redirect(event, "https://example.com");
});
```

**Example:**

```ts
export default defineEventHandler((event) => {
return sendRedirect(event, "https://example.com", 301); // Permanent redirect
return redirect(event, "https://example.com", 301); // Permanent redirect
});
```

### `sendWebResponse(event, response)`
### `removeResponseHeader(event, name)`

Remove a response header by name.

Send a Web besponse object to the client.
**Example:**

```ts
export default defineEventHandler((event) => {
removeResponseHeader(event, "content-type"); // Remove content-type header
});
```

### `setResponseHeader(event, name, value)`

Expand Down
14 changes: 7 additions & 7 deletions docs/2.utils/98.advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ Make a fetch request with the event's context and headers.

Get the request headers object without headers known to cause issues when proxying.

### `proxyRequest(event, target, opts)`
### `proxy(event, target, opts)`

Proxy the incoming request to a target URL.
Make a proxy request to a target URL and send the response back to the client.

### `sendProxy(event, target, opts)`
### `proxyRequest(event, target, opts)`

Make a proxy request to a target URL and send the response back to the client.
Proxy the incoming request to a target URL.

<!-- /automd -->

Expand Down Expand Up @@ -182,15 +182,15 @@ const app = createApp();
const router = createRouter();
router.use('/',
defineEventHandler(async (event) => {
const didHandleCors = handleCors(event, {
const corsRes = handleCors(event, {
origin: '*',
preflight: {
statusCode: 204,
},
methods: '*',
});
if (didHandleCors) {
return;
if (corsRes) {
return corsRes;
}
// Your code here
})
Expand Down
63 changes: 46 additions & 17 deletions src/adapters/node/_internal.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { Readable as NodeReadableStream } from "node:stream";
import type { RawResponse } from "../../types/event";
import type {
NodeHandler,
NodeIncomingMessage,
NodeMiddleware,
NodeServerResponse,
} from "../../types/node";
import type { ResponseBody } from "../../types";
import { _kRaw } from "../../event";
import { createError } from "../../error";
import { splitCookiesString } from "../../utils/cookie";
import {
sanitizeStatusCode,
sanitizeStatusMessage,
} from "../../utils/sanitize";

export function _getBodyStream(
req: NodeIncomingMessage,
Expand All @@ -28,47 +33,71 @@ export function _getBodyStream(
}

export function _sendResponse(
res: NodeServerResponse,
data: RawResponse,
nodeRes: NodeServerResponse,
handlerRes: ResponseBody,
): Promise<void> {
// Web Response
if (handlerRes instanceof Response) {
for (const [key, value] of handlerRes.headers) {
if (key === "set-cookie") {
for (const setCookie of splitCookiesString(value)) {
nodeRes.appendHeader(key, setCookie);
}
} else {
nodeRes.setHeader(key, value);
}
}

if (handlerRes.status) {
nodeRes.statusCode = sanitizeStatusCode(handlerRes.status);
}
if (handlerRes.statusText) {
nodeRes.statusMessage = sanitizeStatusMessage(handlerRes.statusText);
}
if (handlerRes.redirected) {
nodeRes.setHeader("location", handlerRes.url);
}
handlerRes = handlerRes.body; // Next step will send body as stream!
}

// Native Web Streams
// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
if (typeof (data as ReadableStream)?.pipeTo === "function") {
return (data as ReadableStream)
if (typeof (handlerRes as ReadableStream)?.pipeTo === "function") {
return (handlerRes as ReadableStream)
.pipeTo(
new WritableStream({
write: (chunk) => {
res.write(chunk);
nodeRes.write(chunk);
},
}),
)
.then(() => _endResponse(res));
.then(() => _endResponse(nodeRes));
}

// Node.js Readable Streams
// https://nodejs.org/api/stream.html#readable-streams
if (typeof (data as NodeReadableStream)?.pipe === "function") {
if (typeof (handlerRes as NodeReadableStream)?.pipe === "function") {
return new Promise<void>((resolve, reject) => {
// Pipe stream to response
(data as NodeReadableStream).pipe(res);
(handlerRes as NodeReadableStream).pipe(nodeRes);

// Handle stream events (if supported)
if ((data as NodeReadableStream).on) {
(data as NodeReadableStream).on("end", resolve);
(data as NodeReadableStream).on("error", reject);
if ((handlerRes as NodeReadableStream).on) {
(handlerRes as NodeReadableStream).on("end", resolve);
(handlerRes as NodeReadableStream).on("error", reject);
}

// Handle request aborts
res.once("close", () => {
(data as NodeReadableStream).destroy?.();
nodeRes.once("close", () => {
(handlerRes as NodeReadableStream).destroy?.();
// https://react.dev/reference/react-dom/server/renderToPipeableStream
(data as any).abort?.();
(handlerRes as any).abort?.();
});
}).then(() => _endResponse(res));
}).then(() => _endResponse(nodeRes));
}

// Send as string or buffer
return _endResponse(res, data);
return _endResponse(nodeRes, handlerRes);
}

export function _endResponse(
Expand Down
15 changes: 2 additions & 13 deletions src/adapters/node/event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HTTPMethod } from "../../types";
import { RawEvent, type RawResponse } from "../../types/event";
import type { RawEvent } from "../../types/event";
import { splitCookiesString } from "../../utils/cookie";
import { NodeHeadersProxy } from "./_headers";
import {
Expand All @@ -17,8 +17,6 @@ export class NodeEvent implements RawEvent {
_req: NodeIncomingMessage;
_res: NodeServerResponse;

_handled?: boolean;

_originalPath?: string | undefined;

_rawBody?: Promise<undefined | Uint8Array>;
Expand Down Expand Up @@ -125,7 +123,7 @@ export class NodeEvent implements RawEvent {
// -- response --

get handled() {
return this._handled || this._res.writableEnded || this._res.headersSent;
return this._res.writableEnded || this._res.headersSent;
}

get responseCode() {
Expand Down Expand Up @@ -195,13 +193,4 @@ export class NodeEvent implements RawEvent {
});
}
}

sendResponse(data: RawResponse) {
this._handled = true;
return _sendResponse(this._res, data).catch((error) => {
// TODO: better way?
this._handled = false;
throw error;
});
}
}
55 changes: 15 additions & 40 deletions src/adapters/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import type {
NodeServerResponse,
} from "../../types/node";
import { _kRaw } from "../../event";
import { createError, errorToResponse, isError } from "../../error";
import { defineEventHandler, isEventHandler } from "../../handler";
import { EventWrapper } from "../../event";
import { NodeEvent } from "./event";
import { callNodeHandler } from "./_internal";
import { _sendResponse, callNodeHandler } from "./_internal";
import { errorToAppResponse } from "../../app/_response";

/**
* Convert H3 app instance to a NodeHandler with (IncomingMessage, ServerResponse) => void signature.
Expand All @@ -24,48 +24,23 @@ export function toNodeHandler(app: App): NodeHandler {
const nodeHandler: NodeHandler = async function (req, res) {
const rawEvent = new NodeEvent(req, res);
const event = new EventWrapper(rawEvent);
try {
await app.handler(event);
} catch (_error: any) {
const error = createError(_error);
if (!isError(_error)) {
error.unhandled = true;
}

// #754 Make sure hooks see correct status code and message
event[_kRaw].responseCode = error.statusCode;
event[_kRaw].responseMessage = error.statusMessage;

if (app.options.onError) {
await app.options.onError(error, event);
}

if (error.unhandled || error.fatal) {
console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error);
}

if (event[_kRaw].handled) {
const appResponse = await app.handler(event);
await _sendResponse(res, appResponse).catch((sendError) => {
// Possible cases: Stream canceled, headers already sent, etc.
if (res.headersSent || res.writableEnded) {
return;
}

if (app.options.onBeforeResponse && !event._onBeforeResponseCalled) {
await app.options.onBeforeResponse(event, { body: error });
const errRes = errorToAppResponse(sendError, app.options);
if (errRes.status) {
res.statusCode = errRes.status;
}

const response = errorToResponse(error, app.options.debug);

event[_kRaw].responseCode = response.status;
event[_kRaw].responseMessage = response.statusText;

for (const [key, value] of Object.entries(response.headers)) {
event[_kRaw].setResponseHeader(key, value);
}

await event[_kRaw].sendResponse(response.body);

if (app.options.onAfterResponse && !event._onAfterResponseCalled) {
await app.options.onAfterResponse(event, { body: error });
if (errRes.statusText) {
res.statusMessage = errRes.statusText;
}
res.end(errRes.body);
});
if (app.options.onAfterResponse) {
await app.options.onAfterResponse(event, { body: appResponse });
}
};
return nodeHandler;
Expand Down
Loading

0 comments on commit 396aa4b

Please sign in to comment.