Skip to content
Open
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
9 changes: 6 additions & 3 deletions packages/examples/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dev": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/zod_greeter.ts",
"object": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/object.ts",
"greeter": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/greeter.ts",
"greeter_http1": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/greeter_http1.ts",
"zgreeter": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/zod_greeter.ts",
"workflow": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/workflow.ts",
"workflow_client": "RESTATE_LOGGING=debug tsx --tsconfig ./tsconfig.json ./src/workflow_client.ts",
Expand All @@ -24,11 +25,13 @@
},
"dependencies": {
"@restatedev/restate-sdk": "workspace:*",
"@restatedev/restate-sdk-core": "workspace:*",
"@restatedev/restate-sdk-zod": "workspace:*",
"@restatedev/restate-sdk-clients": "workspace:*",
"@restatedev/restate-sdk-core": "workspace:*",
"@restatedev/restate-sdk-testcontainers": "workspace:*",
"@restatedev/restate-sdk-zod": "workspace:*",
"zod": "catalog:"
},
"devDependencies": {}
"devDependencies": {
"testcontainers": "^10.24.1"
}
}
36 changes: 36 additions & 0 deletions packages/examples/node/src/greeter_http1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH
*
* This file is part of the Restate SDK for Node.js/TypeScript,
* which is released under the MIT license.
*
* You can find a copy of the license in file LICENSE in the root
* directory of this repository or package, or at
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
*/

import * as http from "node:http";
import {
createEndpointHandler,
service,
type Context,
} from "@restatedev/restate-sdk";

const greeter = service({
name: "greeter",
handlers: {
greet: async (ctx: Context, name: string) => {
return `Hello ${name}`;
},
},
});

const port = parseInt(process.env.PORT ?? "9080");

const server = http.createServer(
createEndpointHandler({ services: [greeter], bidirectional: true })
);

server.listen(port, () => {
console.log(`Restate HTTP/1.1 server listening on port ${port}`);
});
107 changes: 107 additions & 0 deletions packages/examples/node/test/http1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH
*
* This file is part of the Restate SDK for Node.js/TypeScript,
* which is released under the MIT license.
*
* You can find a copy of the license in file LICENSE in the root
* directory of this repository or package, or at
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
*/

import * as http from "node:http";
import * as net from "node:net";
import { RestateContainer } from "@restatedev/restate-sdk-testcontainers";
import {
createEndpointHandler,
service,
type Context,
} from "@restatedev/restate-sdk";
import * as clients from "@restatedev/restate-sdk-clients";
import {
TestContainers,
Wait,
type StartedTestContainer,
} from "testcontainers";
import { describe, it, beforeAll, afterAll, expect } from "vitest";

const greeter = service({
name: "greeter",
handlers: {
greet: async (ctx: Context, name: string) => {
return `Hello ${name}`;
},
},
});

function defineHttp1Tests(
label: string,
handlerFactory: () => http.RequestListener
) {
describe(label, () => {
let httpServer: http.Server;
let restateContainer: StartedTestContainer;
let rs: clients.Ingress;

beforeAll(async () => {
httpServer = http.createServer(handlerFactory());
await new Promise<void>((resolve, reject) => {
httpServer.listen(0).once("listening", resolve).once("error", reject);
});
const port = (httpServer.address() as net.AddressInfo).port;

await TestContainers.exposeHostPorts(port);

restateContainer = await new RestateContainer()
.withExposedPorts(8080, 9070)
.withWaitStrategy(
Wait.forAll([
Wait.forHttp("/restate/health", 8080),
Wait.forHttp("/health", 9070),
])
)
.start();

const adminUrl = `http://${restateContainer.getHost()}:${restateContainer.getMappedPort(9070)}`;
const res = await fetch(`${adminUrl}/deployments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
uri: `http://host.testcontainers.internal:${port}`,
use_http_11: true,
}),
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Registration failed (${res.status}): ${body}`);
}

const ingressUrl = `http://${restateContainer.getHost()}:${restateContainer.getMappedPort(8080)}`;
rs = clients.connect({ url: ingressUrl });
}, 30_000);

afterAll(async () => {
if (restateContainer) {
await restateContainer.stop();
}
if (httpServer) {
httpServer.close();
}
});

it("Can call a service over HTTP/1.1", async () => {
const client = rs.serviceClient(greeter);
const result = await client.greet("Restate");
expect(result).toBe("Hello Restate");
});
});
}

defineHttp1Tests("HTTP/1.1 endpoint (request-response)", () =>
createEndpointHandler({ services: [greeter] })
);

defineHttp1Tests("HTTP/1.1 endpoint (bidirectional)", () =>
createEndpointHandler({ services: [greeter], bidirectional: true })
);
95 changes: 87 additions & 8 deletions packages/libs/restate-sdk/src/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
*/

import type { Http2ServerRequest, Http2ServerResponse } from "http2";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import type {
VirtualObjectDefinition,
ServiceDefinition,
Expand Down Expand Up @@ -95,12 +96,15 @@ export interface RestateEndpointBase<E> {
/**
* RestateEndpoint encapsulates all the Restate services served by this endpoint.
*
* A RestateEndpoint can either be served as HTTP2 server, using the methods {@link RestateEndpoint.listen} or {@link RestateEndpoint.http2Handler}.
* A RestateEndpoint can be served as:
* - An HTTP/2 server using {@link RestateEndpoint.listen}, {@link RestateEndpoint.http2Handler}
* - An HTTP/1.1 server using {@link RestateEndpoint.http1Handler}
* - A combined HTTP/1.1 + HTTP/2 server using {@link RestateEndpoint.handler}
*
* For Lambda, check {@link LambdaEndpoint}
*
* @example
* A typical endpoint served as HTTP server would look like this:
* A typical endpoint served as HTTP/2 server:
* ```
* import * as restate from "@restatedev/restate-sdk";
*
Expand All @@ -109,6 +113,28 @@ export interface RestateEndpointBase<E> {
* .bind(myService)
* .listen(8000);
* ```
*
* @example
* Using the HTTP/1.1 handler with your own server:
* ```
* import * as http from "node:http";
* import * as restate from "@restatedev/restate-sdk";
*
* const endpoint = restate.endpoint().bind(myService);
* const server = http.createServer(endpoint.http1Handler());
* server.listen(8000);
* ```
*
* @example
* Using the combined handler with an HTTP/2 server that also accepts HTTP/1.1:
* ```
* import * as http2 from "node:http2";
* import * as restate from "@restatedev/restate-sdk";
*
* const endpoint = restate.endpoint().bind(myService);
* const server = http2.createSecureServer({ key, cert, allowHTTP1: true }, endpoint.handler());
* server.listen(8000);
* ```
*/
export interface RestateEndpoint extends RestateEndpointBase<RestateEndpoint> {
/**
Expand Down Expand Up @@ -136,10 +162,63 @@ export interface RestateEndpoint extends RestateEndpointBase<RestateEndpoint> {
listen(port?: number): Promise<number>;

/**
* Returns an http2 server handler. See {@link RestateEndpoint.listen} for more details.
* Returns an http2 server handler.
*
* By default, this handler uses bidirectional streaming (`BIDI_STREAM`).
* Set `bidirectional: false` to use request-response mode (`REQUEST_RESPONSE`).
*
* See {@link RestateEndpoint.listen} for more details.
*/
http2Handler(options?: {
bidirectional?: boolean;
}): (request: Http2ServerRequest, response: Http2ServerResponse) => void;

/**
* Returns an http1 server handler.
*
* By default, this handler operates in request-response protocol mode (`REQUEST_RESPONSE`),
* which buffers the full request before sending the response. This is the safest mode
* for HTTP/1.1 and works across all environments and proxies.
*
* Set `bidirectional: true` to enable bidirectional streaming (`BIDI_STREAM`) for
* HTTP/1.1 servers that support it. Note that some proxies and clients may not
* handle HTTP/1.1 bidirectional streaming correctly.
*
* @example
* ```
* const httpServer = http.createServer(endpoint.http1Handler());
* httpServer.listen(port);
* ```
*/
http1Handler(options?: {
bidirectional?: boolean;
}): (request: IncomingMessage, response: ServerResponse) => void;

/**
* Returns a combined request handler that auto-detects HTTP/1 vs HTTP/2
* requests and dispatches to the appropriate internal handler.
*
* By default (when `bidirectional` is omitted), HTTP/2+ requests use
* bidirectional streaming (`BIDI_STREAM`) and HTTP/1 requests use
* request-response mode (`REQUEST_RESPONSE`).
*
* Set `bidirectional: true` to force `BIDI_STREAM` for all requests,
* or `bidirectional: false` to force `REQUEST_RESPONSE` for all requests.
*
* This is useful with `http2.createSecureServer({ allowHTTP1: true })`, where
* the same server handles both HTTP/1.1 and HTTP/2 connections.
*
* @example
* ```
* const server = http2.createSecureServer(
* { key, cert, allowHTTP1: true },
* endpoint.handler()
* );
* server.listen(port);
* ```
*/
http2Handler(): (
request: Http2ServerRequest,
response: Http2ServerResponse
) => void;
handler(options?: { bidirectional?: boolean }): {
(request: IncomingMessage, response: ServerResponse): void;
(request: Http2ServerRequest, response: Http2ServerResponse): void;
};
}
Loading
Loading