diff --git a/docs/2.utils/2.response.md b/docs/2.utils/2.response.md
index d2f187f5..598756a3 100644
--- a/docs/2.utils/2.response.md
+++ b/docs/2.utils/2.response.md
@@ -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.
@@ -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 "\n
Executing...
\n";
// Do work ...
@@ -128,36 +115,25 @@ async function* work() {
}
// Close out the report
return `
`;
-}
+})
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
-### `sendNoContent(event, code?)`
+### `noContent(event, code?)`
Respond with an empty payload.
-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.
@@ -169,7 +145,7 @@ 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");
});
```
@@ -177,13 +153,21 @@ export default defineEventHandler((event) => {
```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)`
diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md
index 7d42b051..e4ac94c8 100644
--- a/docs/2.utils/98.advanced.md
+++ b/docs/2.utils/98.advanced.md
@@ -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.
@@ -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
})
diff --git a/src/adapters/node/_internal.ts b/src/adapters/node/_internal.ts
index 62337a12..00b66bbd 100644
--- a/src/adapters/node/_internal.ts
+++ b/src/adapters/node/_internal.ts
@@ -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,
@@ -28,47 +33,71 @@ export function _getBodyStream(
}
export function _sendResponse(
- res: NodeServerResponse,
- data: RawResponse,
+ nodeRes: NodeServerResponse,
+ handlerRes: ResponseBody,
): Promise {
+ // 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((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(
diff --git a/src/adapters/node/event.ts b/src/adapters/node/event.ts
index 7751647f..99108471 100644
--- a/src/adapters/node/event.ts
+++ b/src/adapters/node/event.ts
@@ -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 {
@@ -17,8 +17,6 @@ export class NodeEvent implements RawEvent {
_req: NodeIncomingMessage;
_res: NodeServerResponse;
- _handled?: boolean;
-
_originalPath?: string | undefined;
_rawBody?: Promise;
@@ -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() {
@@ -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;
- });
- }
}
diff --git a/src/adapters/node/utils.ts b/src/adapters/node/utils.ts
index 43fb585d..29e09172 100644
--- a/src/adapters/node/utils.ts
+++ b/src/adapters/node/utils.ts
@@ -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.
@@ -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;
diff --git a/src/adapters/web/_internal.ts b/src/adapters/web/_internal.ts
index dc6eb961..aa388c5a 100644
--- a/src/adapters/web/_internal.ts
+++ b/src/adapters/web/_internal.ts
@@ -1,11 +1,13 @@
import type { Readable as NodeReadableStream } from "node:stream";
-import type { App, EventHandler, H3EventContext } from "../../types";
-import type { RawResponse } from "../../types/event";
+import type { App, H3EventContext, ResponseBody } from "../../types";
import { EventWrapper, _kRaw } from "../../event";
import { WebEvent } from "./event";
-import { createError, errorToResponse, isError } from "../../error";
-export function _normalizeResponse(data: RawResponse) {
+type WebNormalizedResponseBody = Exclude;
+
+export function _normalizeResponse(
+ data: ResponseBody,
+): WebNormalizedResponseBody {
// Node.js Readable Streams
// https://nodejs.org/api/stream.html#readable-streams
if (typeof (data as NodeReadableStream)?.pipe === "function") {
@@ -21,7 +23,7 @@ export function _normalizeResponse(data: RawResponse) {
},
});
}
- return data as Exclude;
+ return data as WebNormalizedResponseBody;
}
export function _pathToRequestURL(path: string, headers?: HeadersInit): string {
@@ -36,51 +38,26 @@ export function _pathToRequestURL(path: string, headers?: HeadersInit): string {
export const nullBodyResponses = new Set([101, 204, 205, 304]);
-export async function _callWithWebRequest(
- handler: EventHandler,
+export async function appFetch(
+ app: App,
request: Request,
context?: H3EventContext,
- app?: App,
-) {
+): Promise<{
+ body: WebNormalizedResponseBody;
+ status: Response["status"];
+ statusText: Response["statusText"];
+ headers: Headers;
+}> {
const rawEvent = new WebEvent(request);
- const event = new EventWrapper(rawEvent);
-
- if (context) {
- Object.assign(event.context, context);
- }
-
- let error;
+ const event = new EventWrapper(rawEvent, context);
- try {
- await handler(event);
- } catch (_error: any) {
- error = createError(_error);
- if (!isError(_error)) {
- error.unhandled = true;
- }
- }
-
- if (error) {
- if (error.unhandled || error.fatal) {
- console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error);
- }
- if (app?.options.onError) {
- await app?.options.onError(error, event);
- }
- const errRes = errorToResponse(error, app?.options.debug);
- return {
- status: errRes.status,
- statusText: errRes.statusText,
- headers: new Headers(errRes.headers),
- body: errRes.body,
- };
- }
+ const _appResponseBody = await app.handler(event);
// https://developer.mozilla.org/en-US/docs/Web/API/Response/body
const responseBody =
nullBodyResponses.has(rawEvent.responseCode!) || request.method === "HEAD"
? null
- : _normalizeResponse(rawEvent._responseBody);
+ : _normalizeResponse(_appResponseBody);
return {
status: rawEvent.responseCode,
diff --git a/src/adapters/web/event.ts b/src/adapters/web/event.ts
index 11770183..82efc54d 100644
--- a/src/adapters/web/event.ts
+++ b/src/adapters/web/event.ts
@@ -1,17 +1,14 @@
-import { RawEvent, type RawResponse } from "../../types/event";
-import { HTTPMethod } from "../../types";
+import type { RawEvent } from "../../types/event";
+import type { HTTPMethod } from "../../types";
export class WebEvent implements RawEvent {
static isWeb = true;
_req: Request;
- _handled?: boolean;
-
_path?: string;
_originalPath?: string | undefined;
- _responseBody?: RawResponse;
_responseCode?: number;
_responseMessage?: string;
_responseHeaders: Headers = new Headers();
@@ -107,10 +104,6 @@ export class WebEvent implements RawEvent {
// -- response --
- get handled() {
- return this._handled;
- }
-
get responseCode() {
return this._responseCode || 200;
}
@@ -156,7 +149,6 @@ export class WebEvent implements RawEvent {
}
writeHead(code: number, message?: string) {
- this._handled = true;
if (code) {
this.responseCode = code;
}
@@ -168,9 +160,4 @@ export class WebEvent implements RawEvent {
writeEarlyHints(_hints: Record) {
// noop
}
-
- sendResponse(data: RawResponse) {
- this._handled = true;
- this._responseBody = data;
- }
}
diff --git a/src/adapters/web/index.ts b/src/adapters/web/index.ts
index 7465d26b..9c123561 100644
--- a/src/adapters/web/index.ts
+++ b/src/adapters/web/index.ts
@@ -12,9 +12,6 @@ export {
// Web Context
getWebContext,
- // Call
- callWithWebRequest,
-
// --Plain--
// Plain Handler
diff --git a/src/adapters/web/utils.ts b/src/adapters/web/utils.ts
index 55fc01f6..d8f064f7 100644
--- a/src/adapters/web/utils.ts
+++ b/src/adapters/web/utils.ts
@@ -8,18 +8,16 @@ import type {
import { defineEventHandler } from "../../handler";
import { EventWrapper, _kRaw } from "../../event";
import { WebEvent } from "./event";
-import {
- _callWithWebRequest,
- _normalizeResponse,
- _pathToRequestURL,
-} from "./_internal";
+import { _normalizeResponse, _pathToRequestURL, appFetch } from "./_internal";
/**
* Convert H3 app instance to a WebHandler with (Request, H3EventContext) => Promise signature.
*/
export function toWebHandler(app: App): WebHandler {
- const webHandler: WebHandler = async (request, context) =>
- callWithWebRequest(app.handler, request, context, app);
+ const webHandler: WebHandler = async (request, context) => {
+ const res = await appFetch(app, request, context);
+ return new Response(res.body, res);
+ };
return webHandler;
}
@@ -74,20 +72,6 @@ export function getWebContext(
return raw.getContext();
}
-export async function callWithWebRequest(
- handler: EventHandler,
- request: Request,
- context?: H3EventContext,
- app?: App,
-) {
- const res = await _callWithWebRequest(handler, request, context, app);
- return new Response(res.body, {
- status: res.status,
- statusText: res.statusText,
- headers: res.headers,
- });
-}
-
// ----------------------------
// Plain
// ----------------------------
@@ -97,7 +81,7 @@ export async function callWithWebRequest(
*/
export function toPlainHandler(app: App) {
const handler: PlainHandler = async (request, context) => {
- return callWithPlainRequest(app.handler, request, context, app);
+ return callWithPlainRequest(app, request, context);
};
return handler;
}
@@ -162,20 +146,18 @@ export function fromPlainRequest(
}
export async function callWithPlainRequest(
- handler: EventHandler,
+ app: App,
request: PlainRequest,
context?: H3EventContext,
- app?: App,
): Promise {
- const res = await _callWithWebRequest(
- handler,
+ const res = await appFetch(
+ app,
new Request(_pathToRequestURL(request.path, request.headers), {
method: request.method,
headers: request.headers,
body: request.body,
}),
context,
- app,
);
const setCookie = res.headers.getSetCookie();
diff --git a/src/app.ts b/src/app.ts
deleted file mode 100644
index b6a56af5..00000000
--- a/src/app.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-import type {
- App,
- Stack,
- H3Event,
- EventHandler,
- EventHandlerResolver,
- LazyEventHandler,
- AppOptions,
- InputLayer,
- WebSocketOptions,
- Layer,
-} from "./types";
-import { _kRaw } from "./event";
-import {
- defineLazyEventHandler,
- toEventHandler,
- isEventHandler,
- defineEventHandler,
-} from "./handler";
-import { createError } from "./error";
-import {
- sendWebResponse,
- sendNoContent,
- defaultContentType,
-} from "./utils/response";
-import { isJSONSerializable } from "./utils/internal/object";
-import {
- joinURL,
- getPathname,
- withoutTrailingSlash,
-} from "./utils/internal/path";
-import { MIMES } from "./utils/internal/consts";
-
-/**
- * Create a new H3 app instance.
- */
-export function createApp(options: AppOptions = {}): App {
- const stack: Stack = [];
-
- const handler = createAppEventHandler(stack, options);
-
- const resolve = createResolver(stack);
- handler.__resolve__ = resolve;
-
- const getWebsocket = cachedFn(() => websocketOptions(resolve, options));
-
- const app: App = {
- // @ts-expect-error
- use: (arg1, arg2, arg3) => use(app as App, arg1, arg2, arg3),
- resolve,
- handler,
- stack,
- options,
- get websocket() {
- return getWebsocket();
- },
- };
-
- return app;
-}
-
-export function use(
- app: App,
- arg1: string | EventHandler | InputLayer | InputLayer[],
- arg2?: Partial | EventHandler | EventHandler[],
- arg3?: Partial,
-) {
- if (Array.isArray(arg1)) {
- for (const i of arg1) {
- use(app, i, arg2, arg3);
- }
- } else if (Array.isArray(arg2)) {
- for (const i of arg2) {
- use(app, arg1, i, arg3);
- }
- } else if (typeof arg1 === "string") {
- app.stack.push(
- normalizeLayer({ ...arg3, route: arg1, handler: arg2 as EventHandler }),
- );
- } else if (typeof arg1 === "function") {
- app.stack.push(normalizeLayer({ ...arg2, handler: arg1 as EventHandler }));
- } else {
- app.stack.push(normalizeLayer({ ...arg1 }));
- }
- return app;
-}
-
-export function createAppEventHandler(stack: Stack, options: AppOptions) {
- const spacing = options.debug ? 2 : undefined;
-
- return defineEventHandler(async (event) => {
- // Keep a copy of incoming url
- const _reqPath = event[_kRaw].path || "/";
-
- // Layer path is the path without the prefix
- let _layerPath: string;
-
- // Call onRequest hook
- if (options.onRequest) {
- await options.onRequest(event);
- }
-
- for (const layer of stack) {
- // 1. Remove prefix from path
- if (layer.route.length > 1) {
- if (!_reqPath.startsWith(layer.route)) {
- continue;
- }
- _layerPath = _reqPath.slice(layer.route.length) || "/";
- } else {
- _layerPath = _reqPath;
- }
-
- // 2. Custom matcher
- if (layer.match && !layer.match(_layerPath, event)) {
- continue;
- }
-
- // 3. Update event path with layer path
- event[_kRaw].path = _layerPath;
-
- // 4. Handle request
- const val = await layer.handler(event);
-
- // 5. Try to handle return value
- const _body = val === undefined ? undefined : await val;
- if (_body !== undefined) {
- const _response = { body: _body };
- if (options.onBeforeResponse) {
- event._onBeforeResponseCalled = true;
- await options.onBeforeResponse(event, _response);
- }
- await handleHandlerResponse(event, _response.body, spacing);
- if (options.onAfterResponse) {
- event._onAfterResponseCalled = true;
- await options.onAfterResponse(event, _response);
- }
- return;
- }
-
- // Already handled
- if (event[_kRaw].handled) {
- if (options.onAfterResponse) {
- event._onAfterResponseCalled = true;
- await options.onAfterResponse(event, undefined);
- }
- return;
- }
- }
-
- if (!event[_kRaw].handled) {
- throw createError({
- statusCode: 404,
- statusMessage: `Cannot find any path matching ${event.path || "/"}.`,
- });
- }
-
- if (options.onAfterResponse) {
- event._onAfterResponseCalled = true;
- await options.onAfterResponse(event, undefined);
- }
- });
-}
-
-function createResolver(stack: Stack): EventHandlerResolver {
- return async (path: string) => {
- let _layerPath: string;
- for (const layer of stack) {
- if (layer.route === "/" && !layer.handler.__resolve__) {
- continue;
- }
- if (!path.startsWith(layer.route)) {
- continue;
- }
- _layerPath = path.slice(layer.route.length) || "/";
- if (layer.match && !layer.match(_layerPath, undefined)) {
- continue;
- }
- let res = { route: layer.route, handler: layer.handler };
- if (res.handler.__resolve__) {
- const _res = await res.handler.__resolve__(_layerPath);
- if (!_res) {
- continue;
- }
- res = {
- ...res,
- ..._res,
- route: joinURL(res.route || "/", _res.route || "/"),
- };
- }
- return res;
- }
- };
-}
-
-function normalizeLayer(input: InputLayer) {
- let handler = input.handler;
- // @ts-ignore
- if (handler.handler) {
- // @ts-ignore
- handler = handler.handler;
- }
-
- if (input.lazy) {
- handler = defineLazyEventHandler(handler as LazyEventHandler);
- } else if (!isEventHandler(handler)) {
- handler = toEventHandler(handler, undefined, input.route);
- }
-
- return {
- route: withoutTrailingSlash(input.route),
- match: input.match,
- handler,
- } as Layer;
-}
-
-function handleHandlerResponse(event: H3Event, val: any, jsonSpace?: number) {
- // Empty Content
- if (val === null) {
- return sendNoContent(event);
- }
-
- const valType = typeof val;
-
- // Undefined
- if (valType === "undefined") {
- return sendNoContent(event);
- }
-
- // Text
- if (valType === "string") {
- defaultContentType(event, MIMES.html);
- return event[_kRaw].sendResponse(val);
- }
-
- // Buffer (should be before JSON)
- if (val.buffer) {
- return event[_kRaw].sendResponse(val);
- }
-
- // Error (should be before JSON)
- if (val instanceof Error) {
- throw createError(val);
- }
-
- // JSON
- if (isJSONSerializable(val, valType)) {
- defaultContentType(event, MIMES.json);
- return event[_kRaw].sendResponse(JSON.stringify(val, undefined, jsonSpace));
- }
-
- // BigInt
- if (valType === "bigint") {
- defaultContentType(event, MIMES.json);
- return event[_kRaw].sendResponse(val.toString());
- }
-
- // Web Response
- if (val instanceof Response) {
- return sendWebResponse(event, val);
- }
-
- // Blob
- if (val.arrayBuffer && typeof val.arrayBuffer === "function") {
- return (val as Blob).arrayBuffer().then((arrayBuffer) => {
- defaultContentType(event, val.type);
- return event[_kRaw].sendResponse(Buffer.from(arrayBuffer));
- });
- }
-
- // Symbol or Function is not supported
- if (valType === "symbol" || valType === "function") {
- throw createError({
- statusCode: 500,
- statusMessage: `[h3] Cannot send ${valType} as response.`,
- });
- }
-
- // Other values: direct send
- return event[_kRaw].sendResponse(val);
-}
-
-function cachedFn(fn: () => T): () => T {
- let cache: T;
- return () => {
- if (!cache) {
- cache = fn();
- }
- return cache;
- };
-}
-
-function websocketOptions(
- evResolver: EventHandlerResolver,
- appOptions: AppOptions,
-): WebSocketOptions {
- return {
- ...appOptions.websocket,
- async resolve(info) {
- const pathname = getPathname(info.url || "/");
- const resolved = await evResolver(pathname);
- return resolved?.handler?.__websocket__ || {};
- },
- };
-}
diff --git a/src/app/_handler.ts b/src/app/_handler.ts
new file mode 100644
index 00000000..f55dcfce
--- /dev/null
+++ b/src/app/_handler.ts
@@ -0,0 +1,69 @@
+import type {
+ AppOptions,
+ EventHandler,
+ EventHandlerRequest,
+ ResponseBody,
+ Stack,
+} from "../types";
+import { defineEventHandler } from "../handler";
+import { _kRaw } from "../event";
+import { createError } from "../error";
+import { handleAppResponse } from "./_response";
+
+export function createAppEventHandler(
+ stack: Stack,
+ options: AppOptions,
+): EventHandler> {
+ return defineEventHandler(async (event) => {
+ try {
+ // Keep a copy of incoming url
+ const _reqPath = event[_kRaw].path || "/";
+
+ // Layer path is the path without the prefix
+ let _layerPath: string;
+
+ // Call onRequest hook
+ if (options.onRequest) {
+ await options.onRequest(event);
+ }
+
+ // Run through stack
+ for (const layer of stack) {
+ // 1. Remove prefix from path
+ if (layer.route.length > 1) {
+ if (!_reqPath.startsWith(layer.route)) {
+ continue;
+ }
+ _layerPath = _reqPath.slice(layer.route.length) || "/";
+ } else {
+ _layerPath = _reqPath;
+ }
+
+ // 2. Custom matcher
+ if (layer.match && !layer.match(_layerPath, event)) {
+ continue;
+ }
+
+ // 3. Update event path with layer path
+ event[_kRaw].path = _layerPath;
+
+ // 4. Handle request
+ const val = await layer.handler(event);
+
+ // 5. Handle response
+ const _body = val === undefined ? undefined : await val;
+ if (_body !== undefined) {
+ return handleAppResponse(event, _body, options);
+ }
+ }
+
+ // Throw 404 is no handler in the stack responded
+ throw createError({
+ statusCode: 404,
+ statusMessage: `Cannot find any path matching ${event.path || "/"}.`,
+ });
+ } catch (error: unknown) {
+ return handleAppResponse(event, error, options);
+ }
+ });
+}
diff --git a/src/app/_response.ts b/src/app/_response.ts
new file mode 100644
index 00000000..ec337375
--- /dev/null
+++ b/src/app/_response.ts
@@ -0,0 +1,143 @@
+import type { AppOptions, H3Event, ResponseBody } from "../types";
+import type { AppResponse, H3Error } from "../types/app";
+import { createError } from "../error";
+import { isJSONSerializable } from "../utils/internal/object";
+import { MIMES } from "../utils/internal/consts";
+import { _kRaw } from "../event";
+
+type MaybePromise = T | Promise;
+
+export async function handleAppResponse(
+ event: H3Event,
+ body: unknown,
+ options: AppOptions,
+) {
+ const res = await _normalizeResponseBody(body, options);
+ if (res.error) {
+ if (res.error.unhandled) {
+ console.error("[h3] Unhandled Error:", res.error);
+ }
+ if (options.onError) {
+ try {
+ await options.onError(res.error, event);
+ } catch (hookError) {
+ console.error("[h3] Error while calling `onError` hook:", hookError);
+ }
+ }
+ }
+ if (options.onBeforeResponse) {
+ await options.onBeforeResponse(event, res);
+ }
+ if (res.contentType && !event[_kRaw].getResponseHeader("content-type")) {
+ event[_kRaw].setResponseHeader("content-type", res.contentType);
+ }
+ if (res.headers) {
+ for (const [key, value] of res.headers.entries()) {
+ event[_kRaw].setResponseHeader(key, value);
+ }
+ }
+ if (res.status) {
+ event[_kRaw].responseCode = res.status;
+ }
+ if (res.statusText) {
+ event[_kRaw].responseMessage = res.statusText;
+ }
+ return res.body;
+}
+
+function _normalizeResponseBody(
+ val: unknown,
+ options: AppOptions,
+): MaybePromise {
+ // Empty Content
+ if (val === null || val === undefined) {
+ return { body: "", status: 204 };
+ }
+
+ const valType = typeof val;
+
+ // Text
+ if (valType === "string") {
+ return { body: val as string, contentType: MIMES.html };
+ }
+
+ // Buffer (should be before JSON)
+ if (val instanceof Uint8Array) {
+ return { body: val, contentType: MIMES.octetStream };
+ }
+
+ // Error (should be before JSON)
+ if (val instanceof Error) {
+ return errorToAppResponse(val, options);
+ }
+
+ // JSON
+ if (isJSONSerializable(val, valType)) {
+ return {
+ body: JSON.stringify(val, undefined, options.debug ? 2 : undefined),
+ contentType: MIMES.json,
+ };
+ }
+
+ // BigInt
+ if (valType === "bigint") {
+ return { body: val.toString(), contentType: MIMES.json };
+ }
+
+ // Web Response
+ if (val instanceof Response) {
+ return {
+ body: val.body,
+ headers: val.headers,
+ status: val.status,
+ statusText: val.statusText,
+ };
+ }
+
+ // Blob
+ if (val instanceof Blob) {
+ return val.arrayBuffer().then((arrayBuffer) => {
+ return {
+ contentType: val.type,
+ body: new Uint8Array(arrayBuffer),
+ };
+ });
+ }
+
+ // Symbol or Function is not supported
+ if (valType === "symbol" || valType === "function") {
+ return errorToAppResponse(
+ {
+ statusCode: 500,
+ statusMessage: `[h3] Cannot send ${valType} as response.`,
+ },
+ options,
+ );
+ }
+
+ return {
+ body: val as ResponseBody,
+ };
+}
+
+export function errorToAppResponse(
+ _error: Partial | Error,
+ options: AppOptions,
+): AppResponse {
+ const error = createError(_error as H3Error);
+ return {
+ error,
+ status: error.statusCode,
+ statusText: error.statusMessage,
+ contentType: MIMES.json,
+ body: JSON.stringify({
+ statusCode: error.statusCode,
+ statusMessage: error.statusMessage,
+ data: error.data,
+ stack:
+ options.debug && error.stack
+ ? error.stack.split("\n").map((l) => l.trim())
+ : undefined,
+ }),
+ };
+}
diff --git a/src/app/_utils.ts b/src/app/_utils.ts
new file mode 100644
index 00000000..9617b132
--- /dev/null
+++ b/src/app/_utils.ts
@@ -0,0 +1,124 @@
+import type {
+ Stack,
+ EventHandlerResolver,
+ LazyEventHandler,
+ InputLayer,
+ Layer,
+ AppOptions,
+ WebSocketOptions,
+ App,
+ EventHandler,
+} from "../types";
+import { _kRaw } from "../event";
+import {
+ defineLazyEventHandler,
+ toEventHandler,
+ isEventHandler,
+} from "../handler";
+import {
+ getPathname,
+ joinURL,
+ withoutTrailingSlash,
+} from "../utils/internal/path";
+
+export function use(
+ app: App,
+ arg1: string | EventHandler | InputLayer | InputLayer[],
+ arg2?: Partial | EventHandler | EventHandler[],
+ arg3?: Partial,
+) {
+ if (Array.isArray(arg1)) {
+ for (const i of arg1) {
+ use(app, i, arg2, arg3);
+ }
+ } else if (Array.isArray(arg2)) {
+ for (const i of arg2) {
+ use(app, arg1, i, arg3);
+ }
+ } else if (typeof arg1 === "string") {
+ app.stack.push(
+ normalizeLayer({ ...arg3, route: arg1, handler: arg2 as EventHandler }),
+ );
+ } else if (typeof arg1 === "function") {
+ app.stack.push(normalizeLayer({ ...arg2, handler: arg1 as EventHandler }));
+ } else {
+ app.stack.push(normalizeLayer({ ...arg1 }));
+ }
+ return app;
+}
+
+export function createResolver(stack: Stack): EventHandlerResolver {
+ return async (path: string) => {
+ let _layerPath: string;
+ for (const layer of stack) {
+ if (layer.route === "/" && !layer.handler.__resolve__) {
+ continue;
+ }
+ if (!path.startsWith(layer.route)) {
+ continue;
+ }
+ _layerPath = path.slice(layer.route.length) || "/";
+ if (layer.match && !layer.match(_layerPath, undefined)) {
+ continue;
+ }
+ let res = { route: layer.route, handler: layer.handler };
+ if (res.handler.__resolve__) {
+ const _res = await res.handler.__resolve__(_layerPath);
+ if (!_res) {
+ continue;
+ }
+ res = {
+ ...res,
+ ..._res,
+ route: joinURL(res.route || "/", _res.route || "/"),
+ };
+ }
+ return res;
+ }
+ };
+}
+
+export function normalizeLayer(input: InputLayer) {
+ let handler = input.handler;
+ // @ts-ignore
+ if (handler.handler) {
+ // @ts-ignore
+ handler = handler.handler;
+ }
+
+ if (input.lazy) {
+ handler = defineLazyEventHandler(handler as unknown as LazyEventHandler);
+ } else if (!isEventHandler(handler)) {
+ handler = toEventHandler(handler, undefined, input.route);
+ }
+
+ return {
+ route: withoutTrailingSlash(input.route),
+ match: input.match,
+ handler,
+ } as Layer;
+}
+
+export function resolveWebsocketOptions(
+ evResolver: EventHandlerResolver,
+ appOptions: AppOptions,
+): WebSocketOptions {
+ return {
+ ...appOptions.websocket,
+ async resolve(info) {
+ const pathname = getPathname(info.url || "/");
+ const resolved = await evResolver(pathname);
+ return resolved?.handler?.__websocket__ || {};
+ },
+ };
+}
+
+export function cachedFn(fn: () => T): () => T {
+ let cache: T;
+ return () => {
+ if (!cache) {
+ cache = fn();
+ }
+ return cache;
+ };
+}
diff --git a/src/app/app.ts b/src/app/app.ts
new file mode 100644
index 00000000..8fab4e65
--- /dev/null
+++ b/src/app/app.ts
@@ -0,0 +1,40 @@
+import type { App, Stack, AppOptions } from "../types";
+import { _kRaw } from "../event";
+import { createAppEventHandler } from "./_handler";
+import {
+ cachedFn,
+ createResolver,
+ resolveWebsocketOptions,
+ use,
+} from "./_utils";
+
+/**
+ * Create a new H3 app instance.
+ */
+export function createApp(options: AppOptions = {}): App {
+ const stack: Stack = [];
+
+ const handler = createAppEventHandler(stack, options);
+
+ const resolve = createResolver(stack);
+ handler.__resolve__ = resolve;
+
+ const getWebsocket = cachedFn(() =>
+ resolveWebsocketOptions(resolve, options),
+ );
+
+ const _use = (arg1: any, arg2: any, arg3: any) => use(app, arg1, arg2, arg3);
+
+ const app: App = {
+ use: _use as App["use"],
+ resolve,
+ handler,
+ stack,
+ options,
+ get websocket() {
+ return getWebsocket();
+ },
+ };
+
+ return app;
+}
diff --git a/src/deprecated.ts b/src/deprecated.ts
index 71f5beb7..4d99dd27 100644
--- a/src/deprecated.ts
+++ b/src/deprecated.ts
@@ -15,6 +15,9 @@ import { getBodyStream } from "./utils/body";
import {
appendResponseHeader,
appendResponseHeaders,
+ iterable,
+ noContent,
+ redirect,
setResponseHeader,
setResponseHeaders,
} from "./utils/response";
@@ -30,6 +33,7 @@ import {
readValidatedJSONBody,
} from "./utils/body";
import { defineEventHandler, defineLazyEventHandler } from "./handler";
+import { proxy } from "./utils/proxy";
/** @deprecated Please use `getRequestHeader` */
export const getHeader = getRequestHeader;
@@ -39,10 +43,10 @@ export const getHeaders = getRequestHeaders;
/** @deprecated Directly return stream */
export function sendStream(
- event: H3Event,
+ _event: H3Event,
value: ReadableStream | NodeReadableStream,
) {
- return event[_kRaw].sendResponse(value);
+ return value;
}
/** Please use `defineEventHandler` */
@@ -90,6 +94,24 @@ export const getRequestWebStream = getBodyStream;
/** @deprecated Please use `event.path` instead */
export const getRequestPath = (event: H3Event) => event.path;
+/** @deprecated Use `return iterable()` */
+export const sendIterable = (
+ _event: H3Event,
+ ...args: Parameters
+) => iterable(...args);
+
+/** @deprecated Use `return noContent()` */
+export const sendNoContent = noContent;
+
+/** @deprecated Use `return redirect()` */
+export const sendRedirect = redirect;
+
+/** @deprecated Use `return response` */
+export const sendWebResponse = (response: Response) => response;
+
+/** @deprecated Use `return proxy()` */
+export const sendProxy = proxy;
+
// --- Types ---
/** @deprecated Please use `RequestMiddleware` */
diff --git a/src/error.ts b/src/error.ts
index b7ca6849..956e3f9d 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -1,7 +1,6 @@
import { _kRaw } from "./event";
import { hasProp } from "./utils/internal/object";
import { sanitizeStatusMessage, sanitizeStatusCode } from "./utils/sanitize";
-import { MIMES } from "./utils/internal/consts";
/**
* H3 Runtime Error
@@ -142,31 +141,6 @@ export function createError(
return err;
}
-export function errorToResponse(error: Error | H3Error, debug?: boolean) {
- const h3Error = isError(error) ? error : createError(error);
- const response = {
- error: h3Error,
- status: h3Error.statusCode,
- statusText: h3Error.statusMessage,
- headers: {
- "content-type": MIMES.json,
- },
- body: JSON.stringify(
- {
- statusCode: h3Error.statusCode,
- statusMessage: h3Error.statusMessage,
- data: h3Error.data,
- stack: debug
- ? (h3Error.stack || "").split("\n").map((l) => l.trim())
- : undefined,
- },
- undefined,
- 2,
- ),
- };
- return response;
-}
-
/**
* Checks if the given input is an instance of H3Error.
*
diff --git a/src/event.ts b/src/event.ts
index 2fc47608..9d83a869 100644
--- a/src/event.ts
+++ b/src/event.ts
@@ -1,4 +1,5 @@
-import type { H3Event, RawEvent } from "./types/event";
+import type { H3EventContext, H3Event } from "./types";
+import { RawEvent } from "./types/event";
export const _kRaw: unique symbol = Symbol.for("h3.internal.raw");
@@ -12,8 +13,11 @@ export class EventWrapper implements H3Event {
_onBeforeResponseCalled: boolean | undefined;
_onAfterResponseCalled: boolean | undefined;
- constructor(raw: RawEvent) {
+ constructor(raw: RawEvent, initialContext?: H3EventContext) {
this[_kRaw] = raw;
+ if (initialContext) {
+ Object.assign(this.context, initialContext);
+ }
}
get method() {
diff --git a/src/handler.ts b/src/handler.ts
index c83838c9..f5b22f95 100644
--- a/src/handler.ts
+++ b/src/handler.ts
@@ -86,9 +86,6 @@ async function _callHandler<
if (hooks.onRequest) {
for (const hook of hooks.onRequest) {
await hook(event);
- if (event[_kRaw].handled) {
- return;
- }
}
}
const body = await handler(event);
diff --git a/src/index.ts b/src/index.ts
index d7174339..83652262 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,7 +2,7 @@
export * from "./types";
// App
-export { createApp, use, createAppEventHandler } from "./app";
+export { createApp } from "./app/app";
// Event
export { isEvent } from "./event";
@@ -46,7 +46,6 @@ export {
fromPlainHandler,
toPlainHandler,
fromPlainRequest,
- callWithWebRequest,
callWithPlainRequest,
} from "./adapters/web";
@@ -74,10 +73,6 @@ export {
export {
appendResponseHeader,
appendResponseHeaders,
- sendIterable,
- sendNoContent,
- sendRedirect,
- sendWebResponse,
setResponseHeader,
setResponseHeaders,
setResponseStatus,
@@ -93,7 +88,7 @@ export {
// Proxy
export {
- sendProxy,
+ proxy,
getProxyRequestHeaders,
proxyRequest,
fetchWithEvent,
diff --git a/src/types/app.ts b/src/types/app.ts
index e8a7aca3..cf39b2c9 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,8 +1,15 @@
import type { AdapterOptions as WSOptions } from "crossws";
import type { H3Event } from "./event";
-import type { EventHandler, EventHandlerResolver } from "./handler";
+import type {
+ EventHandler,
+ EventHandlerRequest,
+ EventHandlerResolver,
+ ResponseBody,
+} from "./handler";
import type { H3Error } from "../error";
+type MaybePromise = T | Promise;
+
export type { H3Error } from "../error";
export interface Layer {
@@ -24,6 +31,15 @@ export type InputStack = InputLayer[];
export type Matcher = (url: string, event?: H3Event) => boolean;
+export interface AppResponse {
+ error?: H3Error;
+ body: ResponseBody;
+ contentType?: string;
+ headers?: Headers;
+ status?: number;
+ statusText?: string;
+}
+
export interface AppUse {
(
route: string | string[],
@@ -38,22 +54,22 @@ export type WebSocketOptions = WSOptions;
export interface AppOptions {
debug?: boolean;
- onError?: (error: H3Error, event: H3Event) => any;
- onRequest?: (event: H3Event) => void | Promise;
+ onError?: (error: H3Error, event: H3Event) => MaybePromise;
+ onRequest?: (event: H3Event) => MaybePromise;
onBeforeResponse?: (
event: H3Event,
- response: { body?: unknown },
- ) => void | Promise;
+ response: AppResponse,
+ ) => MaybePromise;
onAfterResponse?: (
event: H3Event,
- response?: { body?: unknown },
- ) => void | Promise;
+ response?: AppResponse,
+ ) => MaybePromise;
websocket?: WebSocketOptions;
}
export interface App {
stack: Stack;
- handler: EventHandler;
+ handler: EventHandler>;
options: AppOptions;
use: AppUse;
resolve: EventHandlerResolver;
diff --git a/src/types/event.ts b/src/types/event.ts
index 4e986d2b..84acd6b4 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -1,4 +1,3 @@
-import type { Readable as NodeReadableStream } from "node:stream";
import type { EventHandlerRequest, H3EventContext, HTTPMethod } from ".";
import type { _kRaw } from "../event";
@@ -26,14 +25,6 @@ export interface H3Event<
_onAfterResponseCalled: boolean | undefined;
}
-export type RawResponse =
- | undefined
- | null
- | Uint8Array
- | string
- | ReadableStream
- | NodeReadableStream;
-
export interface RawEvent {
// -- Context --
getContext: () => Record;
@@ -57,8 +48,6 @@ export interface RawEvent {
// -- Response --
- readonly handled: boolean | undefined;
-
responseCode: number | undefined;
responseMessage: string | undefined;
@@ -69,6 +58,6 @@ export interface RawEvent {
getResponseSetCookie: () => string[];
removeResponseHeader: (key: string) => void;
writeHead: (code: number, message?: string) => void;
- sendResponse: (data?: RawResponse) => void | Promise;
+ // sendResponse: (data?: RawResponse) => void | Promise;
writeEarlyHints: (hints: Record) => void | Promise;
}
diff --git a/src/types/handler.ts b/src/types/handler.ts
index 8692e4b7..9bd11a26 100644
--- a/src/types/handler.ts
+++ b/src/types/handler.ts
@@ -1,7 +1,14 @@
+import type { Readable as NodeReadableStream } from "node:stream";
import type { QueryObject } from "ufo";
import type { H3Event } from "./event";
import type { Hooks as WSHooks } from "crossws";
+export type ResponseBody =
+ | undefined // middleware pass
+ | null // empty content
+ | BodyInit
+ | NodeReadableStream;
+
export type EventHandlerResponse = T | Promise;
export interface EventHandlerRequest {
diff --git a/src/types/index.ts b/src/types/index.ts
index 711afb53..b19cab50 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -23,6 +23,7 @@ export type {
EventHandlerResolver,
EventHandlerResponse,
DynamicEventHandler,
+ ResponseBody,
LazyEventHandler,
InferEventInput,
RequestMiddleware,
diff --git a/src/types/utils/static.ts b/src/types/utils/static.ts
index f6cfc678..b478c9b2 100644
--- a/src/types/utils/static.ts
+++ b/src/types/utils/static.ts
@@ -1,4 +1,4 @@
-import type { RawResponse } from "../event";
+import type { ResponseBody } from "../handler";
export interface StaticAssetMeta {
type?: string;
@@ -20,7 +20,7 @@ export interface ServeStaticOptions {
/**
* This function should resolve asset content
*/
- getContents: (id: string) => RawResponse | Promise;
+ getContents: (id: string) => ResponseBody | Promise;
/**
* Map of supported encodings (compressions) and their file extensions.
diff --git a/src/utils/cache.ts b/src/utils/cache.ts
index 924b2c86..601f19e4 100644
--- a/src/utils/cache.ts
+++ b/src/utils/cache.ts
@@ -38,9 +38,6 @@ export function handleCacheHeaders(
if (cacheMatched) {
event[_kRaw].responseCode = 304;
- if (!event[_kRaw].handled) {
- event[_kRaw].sendResponse();
- }
return true;
}
diff --git a/src/utils/cors.ts b/src/utils/cors.ts
index 5ba2c45b..b7f2a7f4 100644
--- a/src/utils/cors.ts
+++ b/src/utils/cors.ts
@@ -1,7 +1,7 @@
-import type { H3Event } from "../types";
+import type { H3Event, ResponseBody } from "../types";
import type { H3CorsOptions } from "../types/utils/cors";
import { _kRaw } from "../event";
-import { sendNoContent, appendResponseHeaders } from "./response";
+import { noContent, appendResponseHeaders } from "./response";
import {
createAllowHeaderHeaders,
createCredentialsHeaders,
@@ -60,26 +60,28 @@ export function appendCorsHeaders(event: H3Event, options: H3CorsOptions) {
* 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
* })
* );
*/
-export function handleCors(event: H3Event, options: H3CorsOptions): boolean {
+export function handleCors(
+ event: H3Event,
+ options: H3CorsOptions,
+): false | ResponseBody {
const _options = resolveCorsOptions(options);
if (isPreflightRequest(event)) {
appendCorsPreflightHeaders(event, options);
- sendNoContent(event, _options.preflight.statusCode);
- return true;
+ return noContent(event, _options.preflight.statusCode);
}
appendCorsHeaders(event, options);
return false;
diff --git a/src/utils/internal/consts.ts b/src/utils/internal/consts.ts
index e4f30b52..3cfd5e57 100644
--- a/src/utils/internal/consts.ts
+++ b/src/utils/internal/consts.ts
@@ -1,4 +1,5 @@
export const MIMES = {
html: "text/html",
json: "application/json",
+ octetStream: "application/octet-stream",
} as const;
diff --git a/src/utils/internal/event-stream.ts b/src/utils/internal/event-stream.ts
index 2e2363a3..d88cc545 100644
--- a/src/utils/internal/event-stream.ts
+++ b/src/utils/internal/event-stream.ts
@@ -1,4 +1,4 @@
-import type { H3Event } from "../../types";
+import type { H3Event, ResponseBody } from "../../types";
import type { ResponseHeaders } from "../../types/http";
import type { NodeEvent } from "../../types/node";
import type {
@@ -139,10 +139,6 @@ export class EventStream {
// Ignore
}
}
- // check if the stream has been given to the client before closing the connection
- if (this._event[_kRaw].handled && this._handled) {
- this._event[_kRaw].sendResponse();
- }
this._disposed = true;
}
@@ -154,11 +150,11 @@ export class EventStream {
this._writer.closed.then(cb);
}
- async send() {
+ async send(): Promise {
setEventStreamHeaders(this._event);
setResponseStatus(this._event, 200);
this._handled = true;
- this._event[_kRaw].sendResponse(this._transformStream.readable);
+ return this._transformStream.readable;
}
}
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index 47a24ed5..ae9937df 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -1,4 +1,10 @@
-import type { H3EventContext, H3Event, ProxyOptions, Duplex } from "../types";
+import type {
+ H3EventContext,
+ H3Event,
+ ProxyOptions,
+ Duplex,
+ ResponseBody,
+} from "../types";
import { splitCookiesString } from "./cookie";
import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize";
import { _kRaw } from "../event";
@@ -41,7 +47,7 @@ export async function proxyRequest(
opts.headers,
);
- return sendProxy(event, target, {
+ return proxy(event, target, {
...opts,
fetchOptions: {
method,
@@ -56,11 +62,11 @@ export async function proxyRequest(
/**
* Make a proxy request to a target URL and send the response back to the client.
*/
-export async function sendProxy(
+export async function proxy(
event: H3Event,
target: string,
opts: ProxyOptions = {},
-) {
+): Promise {
let response: Response | undefined;
try {
response = await getFetch(opts.fetch)(target, {
@@ -125,19 +131,13 @@ export async function sendProxy(
return (response as any)._data;
}
- // Ensure event is not handled
- if (event[_kRaw].handled) {
- return;
- }
-
// Send at once
if (opts.sendStream === false) {
- const data = new Uint8Array(await response.arrayBuffer());
- return event[_kRaw].sendResponse(data);
+ return new Uint8Array(await response.arrayBuffer());
}
// Send as stream
- return event[_kRaw].sendResponse(response.body);
+ return response.body;
}
/**
diff --git a/src/utils/response.ts b/src/utils/response.ts
index 7acf63b5..7de2937d 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -1,10 +1,13 @@
-import type { H3Event } from "../types";
-import type { ResponseHeaders, ResponseHeaderName } from "../types/http";
-import type { MimeType, StatusCode } from "../types";
+import type {
+ H3Event,
+ ResponseHeaders,
+ ResponseHeaderName,
+ MimeType,
+ StatusCode,
+} from "../types";
import { _kRaw } from "../event";
import { MIMES } from "./internal/consts";
import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize";
-import { splitCookiesString } from "./cookie";
import {
serializeIterableValue,
coerceIterable,
@@ -15,26 +18,15 @@ import {
/**
* Respond with an empty payload.
*
- * Note that calling this function will close the connection and no other data can be sent to the client afterwards.
- *
- * @example
- * export default defineEventHandler((event) => {
- * return sendNoContent(event);
- * });
* @example
* export default defineEventHandler((event) => {
- * sendNoContent(event); // Close the connection
- * console.log("This will not be executed");
+ * return noContent(event);
* });
*
* @param event H3 event
* @param code status code to be send. By default, it is `204 No Content`.
*/
-export function sendNoContent(event: H3Event, code?: StatusCode) {
- if (event[_kRaw].handled) {
- return;
- }
-
+export function noContent(event: H3Event, code?: StatusCode) {
if (!code && event[_kRaw].responseCode !== 200) {
// status code was set with setResponseStatus
code = event[_kRaw].responseCode;
@@ -46,7 +38,7 @@ export function sendNoContent(event: H3Event, code?: StatusCode) {
event[_kRaw].removeResponseHeader("content-length");
}
event[_kRaw].writeHead(_code);
- event[_kRaw].sendResponse();
+ return "";
}
/**
@@ -122,15 +114,15 @@ export function defaultContentType(event: H3Event, type?: MimeType) {
*
* @example
* export default defineEventHandler((event) => {
- * return sendRedirect(event, "https://example.com");
+ * return redirect(event, "https://example.com");
* });
*
* @example
* export default defineEventHandler((event) => {
- * return sendRedirect(event, "https://example.com", 301); // Permanent redirect
+ * return redirect(event, "https://example.com", 301); // Permanent redirect
* });
*/
-export function sendRedirect(
+export function redirect(
event: H3Event,
location: string,
code: StatusCode = 302,
@@ -143,7 +135,7 @@ export function sendRedirect(
const encodedLoc = location.replace(/"/g, "%22");
const html = ``;
defaultContentType(event, MIMES.html);
- return event[_kRaw].sendResponse(html);
+ return html;
}
/**
@@ -298,38 +290,6 @@ export function writeEarlyHints(
return event[_kRaw].writeEarlyHints(hints);
}
-/**
- * Send a Web besponse object to the client.
- */
-export function sendWebResponse(
- event: H3Event,
- response: Response,
-): void | Promise {
- for (const [key, value] of response.headers) {
- if (key === "set-cookie") {
- for (const setCookie of splitCookiesString(value)) {
- event[_kRaw].appendResponseHeader(key, setCookie);
- }
- } else {
- event[_kRaw].setResponseHeader(key, value);
- }
- }
-
- if (response.status) {
- event[_kRaw].responseCode = sanitizeStatusCode(
- response.status,
- event[_kRaw].responseCode,
- );
- }
- if (response.statusText) {
- event[_kRaw].responseMessage = sanitizeStatusMessage(response.statusText);
- }
- if (response.redirected) {
- event[_kRaw].setResponseHeader("location", response.url);
- }
- return event[_kRaw].sendResponse(response.body);
-}
-
/**
* Iterate a source of chunks and send back each chunk in order.
* Supports mixing async work together with emitting chunks.
@@ -344,8 +304,7 @@ export function sendWebResponse(
* @template Value - Test
*
* @example
- * sendIterable(event, work());
- * async function* work() {
+ * return iterable(async function* work() {
* // Open document body
* yield "\nExecuting...
\n";
* // Do work ...
@@ -358,37 +317,34 @@ export function sendWebResponse(
* }
* // Close out the report
* return `
`;
- * }
+ * })
* async function delay(ms) {
* return new Promise(resolve => setTimeout(resolve, ms));
* }
*/
-export function sendIterable(
- event: H3Event,
+export function iterable(
iterable: IterationSource,
options?: {
serializer: IteratorSerializer;
},
-): void | Promise {
+): ReadableStream {
const serializer = options?.serializer ?? serializeIterableValue;
const iterator = coerceIterable(iterable);
- event[_kRaw].sendResponse(
- new ReadableStream({
- async pull(controller) {
- const { value, done } = await iterator.next();
- if (value !== undefined) {
- const chunk = serializer(value);
- if (chunk !== undefined) {
- controller.enqueue(chunk);
- }
- }
- if (done) {
- controller.close();
+ return new ReadableStream({
+ async pull(controller) {
+ const { value, done } = await iterator.next();
+ if (value !== undefined) {
+ const chunk = serializer(value);
+ if (chunk !== undefined) {
+ controller.enqueue(chunk);
}
- },
- cancel() {
- iterator.return?.();
- },
- }),
- );
+ }
+ if (done) {
+ controller.close();
+ }
+ },
+ cancel() {
+ iterator.return?.();
+ },
+ });
}
diff --git a/src/utils/static.ts b/src/utils/static.ts
index 2e79a01f..ae14d4bb 100644
--- a/src/utils/static.ts
+++ b/src/utils/static.ts
@@ -1,4 +1,9 @@
-import type { H3Event, StaticAssetMeta, ServeStaticOptions } from "../types";
+import type {
+ H3Event,
+ StaticAssetMeta,
+ ServeStaticOptions,
+ ResponseBody,
+} from "../types";
import { decodePath } from "ufo";
import { _kRaw } from "../event";
import { createError } from "../error";
@@ -14,7 +19,7 @@ import {
export async function serveStatic(
event: H3Event,
options: ServeStaticOptions,
-): Promise {
+): Promise {
if (event.method !== "GET" && event.method !== "HEAD") {
if (!options.fallthrough) {
throw createError({
@@ -75,7 +80,7 @@ export async function serveStatic(
if (ifNotMatch) {
event[_kRaw].responseCode = 304;
event[_kRaw].responseMessage = "Not Modified";
- return event?.[_kRaw]?.sendResponse("");
+ return "";
}
if (meta.mtime) {
@@ -85,7 +90,7 @@ export async function serveStatic(
if (ifModifiedSinceH && new Date(ifModifiedSinceH) >= mtimeDate) {
event[_kRaw].responseCode = 304;
event[_kRaw].responseMessage = "Not Modified";
- return event?.[_kRaw]?.sendResponse("");
+ return "";
}
if (!event[_kRaw].getResponseHeader("last-modified")) {
@@ -110,11 +115,11 @@ export async function serveStatic(
}
if (event.method === "HEAD") {
- return event?.[_kRaw]?.sendResponse();
+ return "";
}
const contents = await options.getContents(id);
- return event?.[_kRaw]?.sendResponse(contents);
+ return contents;
}
// --- Internal Utils ---
diff --git a/test/_playground.ts b/test/_playground.ts
new file mode 100644
index 00000000..9101cf21
--- /dev/null
+++ b/test/_playground.ts
@@ -0,0 +1,16 @@
+import { createServer } from "node:http";
+import { createApp, createRouter, eventHandler, toNodeHandler } from "../src";
+
+export const app = createApp();
+
+const router = createRouter();
+app.use(router);
+
+router.get(
+ "/",
+ eventHandler(() => Buffer.from("Hello world!
", "utf8")),
+);
+
+createServer(toNodeHandler(app)).listen(3000, () => {
+ console.log("Server listening on http://localhost:3000");
+});
diff --git a/test/app.test.ts b/test/app.test.ts
index 4983c986..6023a37c 100644
--- a/test/app.test.ts
+++ b/test/app.test.ts
@@ -117,7 +117,7 @@ describe("app", () => {
);
const res = await ctx.request.get("/");
- expect(res.text).toBe("Hello world!
");
+ expect(res.body.toString("utf8")).toBe("Hello world!
");
});
it("Node.js Readable Stream", async () => {
diff --git a/test/proxy.test.ts b/test/proxy.test.ts
index bc16b14d..6c7c4bc0 100644
--- a/test/proxy.test.ts
+++ b/test/proxy.test.ts
@@ -10,7 +10,7 @@ import {
readRawBody,
appendResponseHeader,
} from "../src";
-import { sendProxy, proxyRequest } from "../src/utils/proxy";
+import { proxy, proxyRequest } from "../src/utils/proxy";
import { setupTest } from "./_utils";
const spy = vi.spyOn(console, "error");
@@ -18,7 +18,7 @@ const spy = vi.spyOn(console, "error");
describe("proxy", () => {
const ctx = setupTest();
- describe("sendProxy", () => {
+ describe("proxy()", () => {
it("works", async () => {
ctx.app.use(
"/hello",
@@ -27,7 +27,7 @@ describe("proxy", () => {
ctx.app.use(
"/",
eventHandler((event) => {
- return sendProxy(event, ctx.url + "/hello", { fetch });
+ return proxy(event, ctx.url + "/hello", { fetch });
}),
);
@@ -251,7 +251,7 @@ describe("proxy", () => {
ctx.app.use(
"/",
eventHandler((event) => {
- return sendProxy(event, ctx.url + "/setcookies", { fetch });
+ return proxy(event, ctx.url + "/setcookies", { fetch });
}),
);
diff --git a/test/status.test.ts b/test/status.test.ts
index b20174e2..b5d02795 100644
--- a/test/status.test.ts
+++ b/test/status.test.ts
@@ -89,7 +89,7 @@ describe("setResponseStatus", () => {
"/test",
eventHandler((event) => {
setResponseStatus(event, 418, "status-text");
- return null;
+ return "";
}),
);
@@ -103,7 +103,7 @@ describe("setResponseStatus", () => {
expect(res).toMatchObject({
status: 418,
statusText: "status-text",
- body: undefined,
+ body: "",
headers: {},
});
});