mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
[1.3] Bun.serve({ websocket }) types (#20918)
Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -114,8 +114,7 @@ type WebSocketData = {
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
// TypeScript: specify the type of `data`
|
||||
Bun.serve<WebSocketData>({
|
||||
Bun.serve({
|
||||
fetch(req, server) {
|
||||
const cookies = new Bun.CookieMap(req.headers.get("cookie")!);
|
||||
|
||||
@@ -131,8 +130,12 @@ Bun.serve<WebSocketData>({
|
||||
return undefined;
|
||||
},
|
||||
websocket: {
|
||||
// TypeScript: specify the type of ws.data like this
|
||||
data: {} as WebSocketData,
|
||||
|
||||
// handler called when a message is received
|
||||
async message(ws, message) {
|
||||
// ws.data is now properly typed as WebSocketData
|
||||
const user = getUserFromToken(ws.data.authToken);
|
||||
|
||||
await saveMessageToDatabase({
|
||||
@@ -164,7 +167,7 @@ socket.addEventListener("message", event => {
|
||||
Bun's `ServerWebSocket` implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can `.subscribe()` to a topic (specified with a string identifier) and `.publish()` messages to all other subscribers to that topic (excluding itself). This topic-based broadcast API is similar to [MQTT](https://en.wikipedia.org/wiki/MQTT) and [Redis Pub/Sub](https://redis.io/topics/pubsub).
|
||||
|
||||
```ts
|
||||
const server = Bun.serve<{ username: string }>({
|
||||
const server = Bun.serve({
|
||||
fetch(req, server) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/chat") {
|
||||
@@ -179,6 +182,9 @@ const server = Bun.serve<{ username: string }>({
|
||||
return new Response("Hello world");
|
||||
},
|
||||
websocket: {
|
||||
// TypeScript: specify the type of ws.data like this
|
||||
data: {} as { username: string },
|
||||
|
||||
open(ws) {
|
||||
const msg = `${ws.data.username} has entered the chat`;
|
||||
ws.subscribe("the-group-chat");
|
||||
|
||||
@@ -7,7 +7,7 @@ When building a WebSocket server, it's typically necessary to store some identif
|
||||
With [Bun.serve()](https://bun.com/docs/api/websockets#contextual-data), this "contextual data" is set when the connection is initially upgraded by passing a `data` parameter in the `server.upgrade()` call.
|
||||
|
||||
```ts
|
||||
Bun.serve<{ socketId: number }>({
|
||||
Bun.serve({
|
||||
fetch(req, server) {
|
||||
const success = server.upgrade(req, {
|
||||
data: {
|
||||
@@ -20,6 +20,9 @@ Bun.serve<{ socketId: number }>({
|
||||
// ...
|
||||
},
|
||||
websocket: {
|
||||
// TypeScript: specify the type of ws.data like this
|
||||
data: {} as { socketId: number },
|
||||
|
||||
// define websocket handlers
|
||||
async message(ws, message) {
|
||||
// the contextual data is available as the `data` property
|
||||
@@ -41,8 +44,7 @@ type WebSocketData = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
// TypeScript: specify the type of `data`
|
||||
Bun.serve<WebSocketData>({
|
||||
Bun.serve({
|
||||
async fetch(req, server) {
|
||||
// use a library to parse cookies
|
||||
const cookies = parseCookies(req.headers.get("Cookie"));
|
||||
@@ -60,6 +62,9 @@ Bun.serve<WebSocketData>({
|
||||
if (upgraded) return undefined;
|
||||
},
|
||||
websocket: {
|
||||
// TypeScript: specify the type of ws.data like this
|
||||
data: {} as WebSocketData,
|
||||
|
||||
async message(ws, message) {
|
||||
// save the message to a database
|
||||
await saveMessageToDatabase({
|
||||
|
||||
@@ -7,7 +7,7 @@ Bun's server-side `WebSocket` API provides a native pub-sub API. Sockets can be
|
||||
This code snippet implements a simple single-channel chat server.
|
||||
|
||||
```ts
|
||||
const server = Bun.serve<{ username: string }>({
|
||||
const server = Bun.serve({
|
||||
fetch(req, server) {
|
||||
const cookies = req.headers.get("cookie");
|
||||
const username = getUsernameFromCookies(cookies);
|
||||
@@ -17,6 +17,9 @@ const server = Bun.serve<{ username: string }>({
|
||||
return new Response("Hello world");
|
||||
},
|
||||
websocket: {
|
||||
// TypeScript: specify the type of ws.data like this
|
||||
data: {} as { username: string },
|
||||
|
||||
open(ws) {
|
||||
const msg = `${ws.data.username} has entered the chat`;
|
||||
ws.subscribe("the-group-chat");
|
||||
|
||||
@@ -7,7 +7,7 @@ Start a simple WebSocket server using [`Bun.serve`](https://bun.com/docs/api/htt
|
||||
Inside `fetch`, we attempt to upgrade incoming `ws:` or `wss:` requests to WebSocket connections.
|
||||
|
||||
```ts
|
||||
const server = Bun.serve<{ authToken: string }>({
|
||||
const server = Bun.serve({
|
||||
fetch(req, server) {
|
||||
const success = server.upgrade(req);
|
||||
if (success) {
|
||||
|
||||
1467
packages/bun-types/bun.d.ts
vendored
1467
packages/bun-types/bun.d.ts
vendored
File diff suppressed because it is too large
Load Diff
2
packages/bun-types/bun.ns.d.ts
vendored
2
packages/bun-types/bun.ns.d.ts
vendored
@@ -3,5 +3,3 @@ import * as BunModule from "bun";
|
||||
declare global {
|
||||
export import Bun = BunModule;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
5
packages/bun-types/deprecated.d.ts
vendored
5
packages/bun-types/deprecated.d.ts
vendored
@@ -98,6 +98,11 @@ declare module "bun" {
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Serve.Options Bun.Serve.Options<T, R>} instead
|
||||
*/
|
||||
type ServeOptions<T = undefined, R extends string = never> = Serve.Options<T, R>;
|
||||
|
||||
/** @deprecated Use {@link SQL.Query Bun.SQL.Query} */
|
||||
type SQLQuery<T = any> = SQL.Query<T>;
|
||||
|
||||
|
||||
1
packages/bun-types/index.d.ts
vendored
1
packages/bun-types/index.d.ts
vendored
@@ -21,6 +21,7 @@
|
||||
/// <reference path="./redis.d.ts" />
|
||||
/// <reference path="./shell.d.ts" />
|
||||
/// <reference path="./experimental.d.ts" />
|
||||
/// <reference path="./serve.d.ts" />
|
||||
/// <reference path="./sql.d.ts" />
|
||||
/// <reference path="./security.d.ts" />
|
||||
|
||||
|
||||
1272
packages/bun-types/serve.d.ts
vendored
Normal file
1272
packages/bun-types/serve.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
12
src/bake/bake.d.ts
vendored
12
src/bake/bake.d.ts
vendored
@@ -5,8 +5,6 @@
|
||||
// /// <reference path="/path/to/bun/src/bake/bake.d.ts" />
|
||||
|
||||
declare module "bun" {
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
declare namespace Bake {
|
||||
interface Options {
|
||||
/**
|
||||
@@ -369,7 +367,7 @@ declare module "bun" {
|
||||
* A common pattern would be to enforce the object is
|
||||
* `{ default: ReactComponent }`
|
||||
*/
|
||||
render: (request: Request, routeMetadata: RouteMetadata) => Awaitable<Response>;
|
||||
render: (request: Request, routeMetadata: RouteMetadata) => MaybePromise<Response>;
|
||||
/**
|
||||
* Prerendering does not use a request, and is allowed to generate
|
||||
* multiple responses. This is used for static site generation, but not
|
||||
@@ -379,7 +377,7 @@ declare module "bun" {
|
||||
* Note that `import.meta.env.STATIC` will be inlined to true during
|
||||
* a static build.
|
||||
*/
|
||||
prerender?: (routeMetadata: RouteMetadata) => Awaitable<PrerenderResult | null>;
|
||||
prerender?: (routeMetadata: RouteMetadata) => MaybePromise<PrerenderResult | null>;
|
||||
// TODO: prerenderWithoutProps (for partial prerendering)
|
||||
/**
|
||||
* For prerendering routes with dynamic parameters, such as `/blog/:slug`,
|
||||
@@ -409,7 +407,7 @@ declare module "bun" {
|
||||
* return { exhaustive: false };
|
||||
* }
|
||||
*/
|
||||
getParams?: (paramsMetadata: ParamsMetadata) => Awaitable<GetParamIterator>;
|
||||
getParams?: (paramsMetadata: ParamsMetadata) => MaybePromise<GetParamIterator>;
|
||||
/**
|
||||
* When a dynamic build uses static assets, Bun can map content types in the
|
||||
* user's `Accept` header to the different static files.
|
||||
@@ -448,7 +446,7 @@ declare module "bun" {
|
||||
}
|
||||
|
||||
interface DevServerHookEntryPoint {
|
||||
default: (dev: DevServerHookAPI) => Awaitable<void>;
|
||||
default: (dev: DevServerHookAPI) => MaybePromise<void>;
|
||||
}
|
||||
|
||||
interface DevServerHookAPI {
|
||||
@@ -505,7 +503,7 @@ declare module "bun" {
|
||||
}
|
||||
}
|
||||
|
||||
declare interface GenericServeOptions {
|
||||
declare interface BaseServeOptions {
|
||||
/** Add a fullstack web app to this server using Bun Bake */
|
||||
app?: Bake.Options | undefined;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import os from "node:os";
|
||||
import { dirname, isAbsolute, join } from "path";
|
||||
import * as numeric from "_util/numeric.ts";
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export const BREAKING_CHANGES_BUN_1_2 = false;
|
||||
|
||||
export const isMacOS = process.platform === "darwin";
|
||||
@@ -184,7 +182,7 @@ export type DirectoryTree = {
|
||||
| string
|
||||
| Buffer
|
||||
| DirectoryTree
|
||||
| ((opts: { root: string }) => Awaitable<string | Buffer | DirectoryTree>);
|
||||
| ((opts: { root: string }) => Bun.MaybePromise<string | Buffer | DirectoryTree>);
|
||||
};
|
||||
|
||||
export async function makeTree(base: string, tree: DirectoryTree) {
|
||||
|
||||
@@ -605,7 +605,7 @@ describe("@types/bun integration test", () => {
|
||||
},
|
||||
{
|
||||
code: 2345,
|
||||
line: "index.ts:326:29",
|
||||
line: "index.ts:322:29",
|
||||
message:
|
||||
"Argument of type '{ headers: { \"x-bun\": string; }; }' is not assignable to parameter of type 'number'.",
|
||||
},
|
||||
|
||||
@@ -270,10 +270,6 @@ Bun.serve({
|
||||
port: 3000,
|
||||
fetch: () => new Response("ok"),
|
||||
|
||||
// don't do this, use the `tls: {}` options instead
|
||||
key: Bun.file(""), // dont do it!
|
||||
cert: Bun.file(""), // dont do it!
|
||||
|
||||
tls: {
|
||||
key: Bun.file(""), // do this!
|
||||
cert: Bun.file(""), // do this!
|
||||
|
||||
@@ -13,27 +13,35 @@ function tmpdirSync(pattern: string = "bun.test."): string {
|
||||
return fs.mkdtempSync(join(fs.realpathSync.native(os.tmpdir()), pattern));
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch: req => Response.json(req.url),
|
||||
websocket: {
|
||||
message(ws) {
|
||||
expectType(ws.data).is<{ name: string }>();
|
||||
},
|
||||
},
|
||||
} satisfies Bun.ServeOptions<{ name: string }>;
|
||||
|
||||
function expectInstanceOf<T>(value: unknown, constructor: new (...args: any[]) => T): asserts value is T {
|
||||
expect(value).toBeInstanceOf(constructor);
|
||||
}
|
||||
|
||||
function test<T, R extends { [K in keyof R]: Bun.RouterTypes.RouteValue<K & string> }>(
|
||||
function test<T = undefined, R extends string = never>(
|
||||
name: string,
|
||||
serveConfig: Bun.ServeFunctionOptions<T, R>,
|
||||
options: Bun.Serve.Options<T, R>,
|
||||
{
|
||||
onConstructorFailure,
|
||||
overrideExpectBehavior,
|
||||
skip: skipOptions,
|
||||
}: {
|
||||
onConstructorFailure?: (error: Error) => void | Promise<void>;
|
||||
overrideExpectBehavior?: (server: Bun.Server) => void | Promise<void>;
|
||||
overrideExpectBehavior?: (server: NoInfer<Bun.Server<T>>) => void | Promise<void>;
|
||||
skip?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if ("unix" in serveConfig && typeof serveConfig.unix === "string" && process.platform === "win32") {
|
||||
// Skip unix socket tests on Windows
|
||||
return;
|
||||
}
|
||||
const skip = skipOptions || ("unix" in options && typeof options.unix === "string" && process.platform === "win32");
|
||||
|
||||
async function testServer(server: Bun.Server) {
|
||||
async function testServer(server: Bun.Server<T>) {
|
||||
if (overrideExpectBehavior) {
|
||||
await overrideExpectBehavior(server);
|
||||
} else {
|
||||
@@ -45,9 +53,9 @@ function test<T, R extends { [K in keyof R]: Bun.RouterTypes.RouteValue<K & stri
|
||||
}
|
||||
}
|
||||
|
||||
it(name, async () => {
|
||||
it.skipIf(skip)(name, async () => {
|
||||
try {
|
||||
using server = Bun.serve(serveConfig);
|
||||
using server = Bun.serve(options);
|
||||
try {
|
||||
await testServer(server);
|
||||
} finally {
|
||||
@@ -107,18 +115,21 @@ test(
|
||||
test("basic + websocket + upgrade", {
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
expectType<typeof ws>().is<Bun.ServerWebSocket<unknown>>();
|
||||
expectType<typeof ws>().is<Bun.ServerWebSocket<undefined>>();
|
||||
ws.send(message);
|
||||
expectType(message).is<string | Buffer<ArrayBuffer>>();
|
||||
},
|
||||
},
|
||||
|
||||
fetch(req, server) {
|
||||
expectType(req).is<Request>();
|
||||
|
||||
// Upgrade to a ServerWebSocket if we can
|
||||
// This automatically checks for the `Sec-WebSocket-Key` header
|
||||
// meaning you don't have to check headers, you can just call `upgrade()`
|
||||
if (server.upgrade(req)) {
|
||||
// When upgrading, we return undefined since we don't want to send a Response
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
|
||||
return new Response("Regular HTTP response");
|
||||
@@ -127,6 +138,16 @@ test("basic + websocket + upgrade", {
|
||||
|
||||
test("basic + websocket + upgrade + all handlers", {
|
||||
fetch(req, server) {
|
||||
expectType(server.upgrade).is<
|
||||
(
|
||||
req: Request,
|
||||
options: {
|
||||
data?: { name: string };
|
||||
headers?: Bun.HeadersInit;
|
||||
},
|
||||
) => boolean
|
||||
>;
|
||||
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/chat") {
|
||||
if (
|
||||
@@ -147,20 +168,26 @@ test("basic + websocket + upgrade + all handlers", {
|
||||
},
|
||||
|
||||
websocket: {
|
||||
open(ws: Bun.ServerWebSocket<{ name: string }>) {
|
||||
data: {} as { name: string },
|
||||
|
||||
open(ws) {
|
||||
console.log("WebSocket opened");
|
||||
ws.subscribe("the-group-chat");
|
||||
},
|
||||
|
||||
message(ws, message) {
|
||||
expectType(message).is<string | Buffer<ArrayBuffer>>();
|
||||
ws.publish("the-group-chat", `${ws.data.name}: ${message.toString()}`);
|
||||
},
|
||||
|
||||
close(ws, code, reason) {
|
||||
expectType(code).is<number>();
|
||||
expectType(reason).is<string>();
|
||||
ws.publish("the-group-chat", `${ws.data.name} left the chat`);
|
||||
},
|
||||
|
||||
drain(ws) {
|
||||
expectType(ws.data.name).is<string>();
|
||||
console.log("Please send me data. I am ready to receive it.");
|
||||
},
|
||||
|
||||
@@ -201,7 +228,7 @@ test("port 0 + websocket + upgrade", {
|
||||
},
|
||||
websocket: {
|
||||
message(ws) {
|
||||
expectType(ws).is<Bun.ServerWebSocket<unknown>>();
|
||||
expectType(ws).is<Bun.ServerWebSocket<undefined>>();
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -269,9 +296,13 @@ test(
|
||||
{
|
||||
unix: `${tmpdirSync()}/bun.sock`,
|
||||
fetch(req, server) {
|
||||
server.upgrade(req);
|
||||
if (server.upgrade(req)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Response();
|
||||
},
|
||||
websocket: { message() {} },
|
||||
},
|
||||
{
|
||||
overrideExpectBehavior: server => {
|
||||
@@ -504,11 +535,10 @@ test("basic websocket upgrade and ws publish/subscribe to topics", {
|
||||
|
||||
test(
|
||||
"port with unix socket (is a type error)",
|
||||
// This prettier-ignore exists because between TypeScript 5.8 and 5.9, the location of the error message changed, so
|
||||
// to satisfy both we can just keep what would have been the two erroring lines on the same line
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
{ unix: `${tmpdirSync()}/bun.sock`, port: 0,
|
||||
// @ts-expect-error Cannot pass unix and port
|
||||
{
|
||||
unix: `${tmpdirSync()}/bun.sock`,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response();
|
||||
},
|
||||
@@ -524,10 +554,10 @@ test(
|
||||
|
||||
test(
|
||||
"port with unix socket with websocket + upgrade (is a type error)",
|
||||
// Prettier ignore exists for same reason as above
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
{ unix: `${tmpdirSync()}/bun.sock`, port: 0,
|
||||
// @ts-expect-error cannot pass unix and port at same time
|
||||
{
|
||||
unix: `${tmpdirSync()}/bun.sock`,
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
server.upgrade(req);
|
||||
if (Math.random() > 0.5) return undefined;
|
||||
@@ -543,3 +573,246 @@ test(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test("hostname: 0.0.0.0 (default - listen on all interfaces)", {
|
||||
hostname: "0.0.0.0",
|
||||
fetch() {
|
||||
return new Response("listening on all interfaces");
|
||||
},
|
||||
});
|
||||
|
||||
test("hostname: 127.0.0.1 (localhost only)", {
|
||||
hostname: "127.0.0.1",
|
||||
fetch() {
|
||||
return new Response("listening on localhost only");
|
||||
},
|
||||
});
|
||||
|
||||
test("hostname: localhost", {
|
||||
hostname: "localhost",
|
||||
fetch() {
|
||||
return new Response("listening on localhost");
|
||||
},
|
||||
});
|
||||
|
||||
test(
|
||||
"hostname: custom IPv4 address",
|
||||
{
|
||||
hostname: "192.168.1.100",
|
||||
fetch() {
|
||||
return new Response("custom hostname");
|
||||
},
|
||||
},
|
||||
{
|
||||
onConstructorFailure: error => {
|
||||
expect(error.message).toContain("Failed to start server");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test("port: number type", {
|
||||
port: 3000,
|
||||
fetch() {
|
||||
return new Response("port as number");
|
||||
},
|
||||
});
|
||||
|
||||
test("port: string type", {
|
||||
port: "3001",
|
||||
fetch() {
|
||||
return new Response("port as string");
|
||||
},
|
||||
});
|
||||
|
||||
test("port: 0 (random port assignment)", {
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("random port");
|
||||
},
|
||||
});
|
||||
|
||||
test(
|
||||
"port: from environment variable",
|
||||
{
|
||||
port: process.env.PORT || "3002",
|
||||
fetch() {
|
||||
return new Response("port from env");
|
||||
},
|
||||
},
|
||||
{
|
||||
overrideExpectBehavior: server => {
|
||||
expect(server.port).toBeGreaterThan(0);
|
||||
expect(server.url).toBeDefined();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test("reusePort: false (default)", {
|
||||
reusePort: false,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("reusePort false");
|
||||
},
|
||||
});
|
||||
|
||||
test("reusePort: true", {
|
||||
reusePort: true,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("reusePort true");
|
||||
},
|
||||
});
|
||||
|
||||
test("ipv6Only: false (default)", {
|
||||
ipv6Only: false,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("ipv6Only false");
|
||||
},
|
||||
});
|
||||
|
||||
test("idleTimeout: default (10 seconds)", {
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("default idleTimeout");
|
||||
},
|
||||
});
|
||||
|
||||
test("idleTimeout: custom value (30 seconds)", {
|
||||
idleTimeout: 30,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("custom idleTimeout");
|
||||
},
|
||||
});
|
||||
|
||||
test("idleTimeout: 0 (no timeout)", {
|
||||
idleTimeout: 0,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("no idleTimeout");
|
||||
},
|
||||
});
|
||||
|
||||
test("maxRequestBodySize: default (128MB)", {
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("default maxRequestBodySize");
|
||||
},
|
||||
});
|
||||
|
||||
test("maxRequestBodySize: custom small value", {
|
||||
maxRequestBodySize: 1024 * 1024, // 1MB
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("small maxRequestBodySize");
|
||||
},
|
||||
});
|
||||
|
||||
test("maxRequestBodySize: custom large value", {
|
||||
maxRequestBodySize: 1024 * 1024 * 1024, // 1GB
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("large maxRequestBodySize");
|
||||
},
|
||||
});
|
||||
|
||||
test("development: true", {
|
||||
development: true,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("development mode on");
|
||||
},
|
||||
});
|
||||
|
||||
test("development: false", {
|
||||
development: false,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("development mode off");
|
||||
},
|
||||
});
|
||||
|
||||
test("development: defaults to process.env.NODE_ENV !== 'production'", {
|
||||
development: process.env.NODE_ENV !== "production",
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("development from env");
|
||||
},
|
||||
});
|
||||
|
||||
test(
|
||||
"error callback handles errors",
|
||||
{
|
||||
port: 0,
|
||||
fetch() {
|
||||
throw new Error("Test error");
|
||||
},
|
||||
error(error) {
|
||||
return new Response(`Error handled: ${error.message}`, { status: 500 });
|
||||
},
|
||||
},
|
||||
{
|
||||
overrideExpectBehavior: async server => {
|
||||
const res = await fetch(server.url);
|
||||
expect(res.status).toBe(500);
|
||||
expect(await res.text()).toBe("Error handled: Test error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"error callback with async handler",
|
||||
{
|
||||
port: 0,
|
||||
fetch() {
|
||||
throw new Error("Async test error");
|
||||
},
|
||||
async error(error) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
return new Response(`Async error handled: ${error.message}`, { status: 503 });
|
||||
},
|
||||
},
|
||||
{
|
||||
overrideExpectBehavior: async server => {
|
||||
const res = await fetch(server.url);
|
||||
expect(res.status).toBe(503);
|
||||
expect(await res.text()).toBe("Async error handled: Async test error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test("id: custom server identifier", {
|
||||
id: "my-custom-server-id",
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("server with custom id");
|
||||
},
|
||||
});
|
||||
|
||||
test("id: null (no identifier)", {
|
||||
id: null,
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("server with null id");
|
||||
},
|
||||
});
|
||||
|
||||
test("multiple properties combined", {
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
reusePort: true,
|
||||
idleTimeout: 20,
|
||||
maxRequestBodySize: 1024 * 1024 * 10, // 10MB
|
||||
development: true,
|
||||
id: "combined-test-server",
|
||||
fetch(req) {
|
||||
return Response.json({
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
},
|
||||
error(error) {
|
||||
return new Response(`Combined server error: ${error.message}`, { status: 500 });
|
||||
},
|
||||
});
|
||||
|
||||
86
test/integration/bun-types/fixture/serve.ts
Normal file
86
test/integration/bun-types/fixture/serve.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// This file is merely types only, you (probably) want to put the tests in ./serve-types.test.ts instead
|
||||
|
||||
import { expectType } from "./utilities";
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/:id/:test": req => {
|
||||
expectType(req.params).is<{ id: string; test: string }>();
|
||||
},
|
||||
},
|
||||
fetch: () => new Response("hello"),
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
expectType(ws.data).is<undefined>();
|
||||
expectType(message).is<string | Buffer<ArrayBuffer>>();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const s1 = Bun.serve({
|
||||
routes: {
|
||||
"/ws/:name": req => {
|
||||
expectType(req.params.name).is<string>();
|
||||
|
||||
s1.upgrade(req, {
|
||||
data: { name: req.params.name },
|
||||
});
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
data: {} as { name: string },
|
||||
|
||||
message(ws) {
|
||||
ws.send(JSON.stringify(ws.data));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const s2 = Bun.serve({
|
||||
routes: {
|
||||
"/ws/:name": req => {
|
||||
expectType(req.params.name).is<string>();
|
||||
|
||||
// @ts-expect-error - Should error because data was not passed
|
||||
s2.upgrade(req, {});
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
data: {} as { name: string },
|
||||
message(ws) {
|
||||
expectType(ws.data).is<{ name: string }>();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const s3 = Bun.serve({
|
||||
routes: {
|
||||
"/ws/:name": req => {
|
||||
expectType(req.params.name).is<string>();
|
||||
|
||||
// @ts-expect-error - Should error because data and object was not passed
|
||||
s3.upgrade(req);
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
data: {} as { name: string },
|
||||
message(ws) {
|
||||
expectType(ws.data).is<{ name: string }>();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const s4 = Bun.serve({
|
||||
routes: {
|
||||
"/ws/:name": req => {
|
||||
expectType(req.params.name).is<string>();
|
||||
|
||||
s4.upgrade(req);
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
message(ws) {
|
||||
expectType(ws.data).is<undefined>();
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user