Skip to content
File

Blob: src/cloudflare/node.ts

typescript116 lines
1// Copyright (c) 2024 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import {
5 portMapper,
6 type FetchHandler as Fetcher,
7} from 'cloudflare-internal:http';
8 
9interface ServerDescriptor {
10 port?: number | null | undefined;
11}
12 
13interface NodeStyleServer {
14 listen(...args: unknown[]): this;
15 address(): { port?: number | null | undefined };
16}
17 
18function validatePort(port: unknown): number {
19 if (
20 !Number.isFinite(port) ||
21 (port as number) < 0 ||
22 (port as number) > 65535
23 ) {
24 throw new Error('Failed to determine port for server');
25 }
26 return port as number;
27}
28 
29export async function handleAsNodeRequest(
30 desc: number | ServerDescriptor,
31 request: Request,
32 env?: unknown,
33 ctx?: unknown
34): Promise<Response> {
35 if (typeof desc === 'number') {
36 desc = { port: desc };
37 }
38 // While TypeScript will complain if `desc` is null or undefined,
39 // JavaScript does not enforce this, so we need to check at runtime.
40 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
41 const port = validatePort(desc?.port);
42 const instance = portMapper.get(port);
43 if (!instance) {
44 const error = new Error(
45 `Http server with port ${port} not found. This is likely a bug with your code. ` +
46 `You should check if server.listen() was called with the same port (${port})`
47 );
48 // @ts-expect-error TS2339 We're imitating Node.js errors.
49 error.code = 'ERR_INVALID_ARG_VALUE';
50 throw error;
51 }
52 return await instance.fetch(request, env, ctx);
53}
54 
55export function httpServerHandler(
56 desc: number | ServerDescriptor | NodeStyleServer
57): Fetcher {
58 if (typeof desc === 'number') {
59 desc = { port: desc };
60 }
61 // While the TypeScript type system prevents `desc` from being null or undefined,
62 // JavaScript does not, so we need to check for it at runtime.
63 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
64 if (desc == null) {
65 throw new Error('Server descriptor cannot be null or undefined');
66 }
67 
68 // The desc can be a ServerDescriptor or a Server (where "Server" is defined
69 // as a type that has a "listen()" method and an "address()" method).
70 let port: number | null = null;
71 
72 // If there is no port defined and desc has a listen method, we will try to
73 // access it as a Server, calling listen() if necessary to determine the port.
74 if (
75 (desc as ServerDescriptor).port == null &&
76 typeof (desc as NodeStyleServer).listen === 'function'
77 ) {
78 const server = desc as NodeStyleServer;
79 // First, let's see if the server-like thing already has a port.
80 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
81 let serverPort = server.address()?.port;
82 if (typeof serverPort === 'number') {
83 // Nice, we're already bound to a port.
84 port = serverPort;
85 } else {
86 // We're not yet bound to a port. Try calling listen() to start the server
87 // and determine the port.
88 server.listen();
89 // Did it work? We do expect the listen in this case to synchronously
90 // assign the port so we can check it immediately.
91 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
92 serverPort = server.address()?.port;
93 if (typeof serverPort === 'number') {
94 // Nice. We've got a port now.
95 port = serverPort;
96 }
97 }
98 } else if (typeof (desc as ServerDescriptor).port === 'number') {
99 // We're a ServerDescriptor with a port defined.
100 port = (desc as ServerDescriptor).port as number;
101 }
102 
103 port = validatePort(port);
104 
105 return {
106 async fetch(req: Request, env?: unknown, ctx?: unknown): Promise<Response> {
107 return await handleAsNodeRequest(
108 { port: validatePort(port) },
109 req,
110 env,
111 ctx
112 );
113 },
114 };
115}