Skip to content

feat(node): support http2 #58

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ dist
*.crt
*.key
*.pem
.tmp
tsconfig.tsbuildinfo
3 changes: 2 additions & 1 deletion docs/1.guide/5.options.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If enabled, no server listening message will be printed (enabled by default when

The protocol to use for the server.

Possible values are `http` and `https`.
Possible values are `http` or `https`.

If `protocol` is not set, Server will use `http` as the default protocol or `https` if both `tls.cert` and `tls.key` options are provided.

Expand Down Expand Up @@ -134,6 +134,7 @@ serve({
node: {
maxHeadersize: 16384 * 2, // Double default
ipv6Only: true, // Disable dual-stack support
// http2: true // Enable HTTP2 support (requires top level tls option)
},
fetch: () => new Response("👋 Hello there!"),
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"undici": "^7.8.0",
"vitest": "^3.1.2"
},
"packageManager": "pnpm@10.9.0",
Expand Down
3 changes: 2 additions & 1 deletion playground/app.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { serve } from "srvx";

serve({
// tls: { cert: "server.crt", key: "server.key" },
node: { http2: true },
tls: { cert: "server.crt", key: "server.key" },
fetch(_request) {
return new Response(
/*html */ `
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 25 additions & 20 deletions src/_node-compat/headers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import type NodeHttp from "node:http";
import { splitSetCookieString } from "cookie-es";
import { kNodeInspect } from "./_common.ts";

import type { NodeServerRequest, NodeServerResponse } from "../types.ts";

export const NodeRequestHeaders: {
new (nodeCtx: {
req: NodeHttp.IncomingMessage;
res?: NodeHttp.ServerResponse;
req: NodeServerRequest;
res?: NodeServerResponse;
}): globalThis.Headers;
} = /* @__PURE__ */ (() => {
const _Headers = class Headers implements globalThis.Headers {
_node: { req: NodeHttp.IncomingMessage; res?: NodeHttp.ServerResponse };
_node: {
req: NodeServerRequest;
res?: NodeServerResponse;
};

constructor(nodeCtx: {
req: NodeHttp.IncomingMessage;
res?: NodeHttp.ServerResponse;
}) {
constructor(nodeCtx: { req: NodeServerRequest; res?: NodeServerResponse }) {
this._node = nodeCtx;
}

append(name: string, value: string): void {
name = name.toLowerCase();
name = validateHeader(name);
const _headers = this._node.req.headers;
const _current = _headers[name];
if (_current) {
Expand All @@ -34,12 +35,12 @@ export const NodeRequestHeaders: {
}

delete(name: string): void {
name = name.toLowerCase();
name = validateHeader(name);
this._node.req.headers[name] = undefined;
}

get(name: string): string | null {
name = name.toLowerCase();
name = validateHeader(name);
const rawValue = this._node.req.headers[name];
if (rawValue === undefined) {
return null;
Expand All @@ -56,12 +57,12 @@ export const NodeRequestHeaders: {
}

has(name: string): boolean {
name = name.toLowerCase();
name = validateHeader(name);
return !!this._node.req.headers[name];
}

set(name: string, value: string): void {
name = name.toLowerCase();
name = validateHeader(name);
this._node.req.headers[name] = value;
}

Expand Down Expand Up @@ -144,17 +145,14 @@ export const NodeRequestHeaders: {

export const NodeResponseHeaders: {
new (nodeCtx: {
req?: NodeHttp.IncomingMessage;
res: NodeHttp.ServerResponse;
req?: NodeServerRequest;
res: NodeServerResponse;
}): globalThis.Headers;
} = /* @__PURE__ */ (() => {
const _Headers = class Headers implements globalThis.Headers {
_node: { req?: NodeHttp.IncomingMessage; res: NodeHttp.ServerResponse };
_node: { req?: NodeServerRequest; res: NodeServerResponse };

constructor(nodeCtx: {
req?: NodeHttp.IncomingMessage;
res: NodeHttp.ServerResponse;
}) {
constructor(nodeCtx: { req?: NodeServerRequest; res: NodeServerResponse }) {
this._node = nodeCtx;
}

Expand Down Expand Up @@ -275,3 +273,10 @@ function _normalizeValue(
}
return typeof value === "string" ? value : String(value ?? "");
}

function validateHeader(name: string): string {
if (name[0] === ":") {
throw new TypeError("Invalid name");
}
return name.toLowerCase();
}
17 changes: 11 additions & 6 deletions src/_node-compat/request.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type NodeHttp from "node:http";
import type NodeStream from "node:stream";
import type { ServerRequest, ServerRuntimeContext } from "../types.ts";
import { kNodeInspect } from "./_common.ts";
import { NodeRequestHeaders } from "./headers.ts";
import { NodeRequestURL } from "./url.ts";

import type NodeStream from "node:stream";
import type {
NodeServerRequest,
NodeServerResponse,
ServerRequest,
ServerRuntimeContext,
} from "../types.ts";

export type NodeRequestContext = {
req: NodeHttp.IncomingMessage;
res?: NodeHttp.ServerResponse;
req: NodeServerRequest;
res?: NodeServerResponse;
upgrade?: {
socket: NodeStream.Duplex;
header: Buffer;
Expand All @@ -28,7 +33,7 @@ export const NodeRequest = /* @__PURE__ */ (() => {
#textBody?: Promise<string>;
#bodyStream?: undefined | ReadableStream<Uint8Array>;

_node: { req: NodeHttp.IncomingMessage; res?: NodeHttp.ServerResponse };
_node: { req: NodeServerRequest; res?: NodeServerResponse };
runtime: ServerRuntimeContext;

constructor(nodeCtx: NodeRequestContext) {
Expand Down
3 changes: 2 additions & 1 deletion src/_node-compat/response.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { splitSetCookieString } from "cookie-es";

import type NodeHttp from "node:http";
import type { Readable as NodeReadable } from "node:stream";
import { splitSetCookieString } from "cookie-es";

export type NodeResponse = InstanceType<typeof NodeResponse>;

Expand Down
35 changes: 22 additions & 13 deletions src/_node-compat/send.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { splitSetCookieString } from "cookie-es";

import type { Duplex, Readable as NodeReadable } from "node:stream";
import type NodeHttp from "node:http";
import NodeHttp2 from "node:http2";
import type { NodeServerResponse } from "../types.ts";
import type { NodeResponse } from "./response.ts";
import type { Duplex, Readable as NodeReadable } from "node:stream";
import { splitSetCookieString } from "cookie-es";

export async function sendNodeResponse(
nodeRes: NodeHttp.ServerResponse,
nodeRes: NodeServerResponse,
webRes: Response | NodeResponse,
): Promise<void> {
if (!webRes) {
Expand All @@ -16,7 +19,7 @@ export async function sendNodeResponse(
if ((webRes as NodeResponse).nodeResponse) {
const res = (webRes as NodeResponse).nodeResponse();
if (!nodeRes.headersSent) {
nodeRes.writeHead(res.status, res.statusText, res.headers.flat());
nodeRes.writeHead(res.status, res.statusText, res.headers.flat() as any);
}
if (res.body) {
if (res.body instanceof ReadableStream) {
Expand All @@ -25,7 +28,8 @@ export async function sendNodeResponse(
(res.body as NodeReadable).pipe(nodeRes);
return new Promise((resolve) => nodeRes.on("close", resolve));
}
nodeRes.write(res.body);
// TODO: FastResponse write with http2?
(nodeRes as NodeHttp.ServerResponse).write(res.body);
}
return endNodeResponse(nodeRes);
}
Expand All @@ -42,11 +46,16 @@ export async function sendNodeResponse(
}

if (!nodeRes.headersSent) {
nodeRes.writeHead(
webRes.status || 200,
webRes.statusText,
headerEntries.flat(),
);
// TODO: use faster method to check http2
if (nodeRes instanceof NodeHttp2.Http2ServerResponse) {
nodeRes.writeHead(webRes.status || 200, headerEntries.flat() as any);
} else {
nodeRes.writeHead(
webRes.status || 200,
webRes.statusText,
headerEntries.flat() as any,
);
}
}

return webRes.body
Expand Down Expand Up @@ -76,13 +85,13 @@ export async function sendNodeUpgradeResponse(
});
}

function endNodeResponse(nodeRes: NodeHttp.ServerResponse) {
function endNodeResponse(nodeRes: NodeServerResponse) {
return new Promise<void>((resolve) => nodeRes.end(resolve));
}

export function streamBody(
stream: ReadableStream,
nodeRes: NodeHttp.ServerResponse,
nodeRes: NodeServerResponse,
): Promise<void> | void {
// stream is already destroyed
if (nodeRes.destroyed) {
Expand All @@ -108,7 +117,7 @@ export function streamBody(
if (done) {
// End the response
nodeRes.end();
} else if (nodeRes.write(value)) {
} else if ((nodeRes as NodeHttp.ServerResponse).write(value)) {
// Continue reading recursively
reader.read().then(streamHandle, streamCancel);
} else {
Expand Down
24 changes: 13 additions & 11 deletions src/_node-compat/url.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type NodeHttp from "node:http";
import { kNodeInspect } from "./_common.ts";

import type { NodeServerRequest, NodeServerResponse } from "../types.ts";

export const NodeRequestURL: {
new (nodeCtx: {
req: NodeHttp.IncomingMessage;
res?: NodeHttp.ServerResponse;
}): URL;
new (nodeCtx: { req: NodeServerRequest; res?: NodeServerResponse }): URL;
} = /* @__PURE__ */ (() => {
const _URL = class URL implements Partial<globalThis.URL> {
_node: { req: NodeHttp.IncomingMessage; res?: NodeHttp.ServerResponse };
_node: {
req: NodeServerRequest;
res?: NodeServerResponse;
};

_hash = "";
_username = "";
Expand All @@ -21,10 +22,7 @@ export const NodeRequestURL: {
_search?: string;
_searchParams?: URLSearchParams;

constructor(nodeCtx: {
req: NodeHttp.IncomingMessage;
res?: NodeHttp.ServerResponse;
}) {
constructor(nodeCtx: { req: NodeServerRequest; res?: NodeServerResponse }) {
this._node = nodeCtx;
}

Expand Down Expand Up @@ -54,7 +52,11 @@ export const NodeRequestURL: {

// host
get host() {
return this._node.req.headers.host || "";
return (
this._node.req.headers.host ||
(this._node.req.headers[":authority"] as string) ||
""
);
}
set host(value: string) {
this._hostname = undefined;
Expand Down
Loading
Loading