Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: overhaul app event handler #792

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading