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
50 changes: 41 additions & 9 deletions dev/src/server/adk_api_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,45 @@ interface ServerOptions {
registerProcessors?: (tracerProvider: TracerProvider) => void;
}

/**
* A Node system error: an `Error` that carries an errno string in `code`.
*
* The `@types/node` package declares this shape only as
* `NodeJS.ErrnoException`, in an ambient namespace that the repository's
* `no-undef` lint rule rejects.
*/
interface SystemError extends Error {
code?: string;
}

/**
* Converts a `listen()` failure into the error the caller reports.
*
* The CLI prints only `error.message`, so a cause the operator can act on has
* to be in the message itself. An errno with no better wording than the system
* error is returned unchanged.
*/
export function toListenError(
err: SystemError,
host: string,
port: number,
): Error {
switch (err.code) {
case 'EADDRINUSE':
return new Error(`Port ${port} is already in use`, {cause: err});
case 'EACCES':
return new Error(
`Permission denied binding ${host}:${port}. Ports below 1024 need ` +
`elevated privileges. Windows also reserves blocks of ports; list ` +
`them with "netsh interface ipv4 show excludedportrange ` +
`protocol=tcp". Use a different port, or port 0 for any free port.`,
{cause: err},
);
default:
return err;
}
}

export class AdkApiServer {
private readonly host: string;
private readonly port: number;
Expand Down Expand Up @@ -973,15 +1012,8 @@ export class AdkApiServer {
}
});

this.server.on('error', (err: unknown) => {
if ((err as {code: string}).code === 'EADDRINUSE') {
const error = new Error();
error.cause = err;
error.message = `Port ${this.port} is already in use`;
reject(error);
} else {
reject(err);
}
this.server.on('error', (err: SystemError) => {
reject(toListenError(err, this.host, this.port));
});
});
}
Expand Down
47 changes: 47 additions & 0 deletions dev/test/server/adk_api_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {z} from 'zod';
import {
A2A_AUTH_TOKEN_ENV_VAR,
AdkApiServer,
toListenError,
} from '../../src/server/adk_api_server.js';
import {AgentLoader} from '../../src/utils/agent_loader.js';

Expand Down Expand Up @@ -1256,3 +1257,49 @@ describe('AdkWebServer', () => {
});
});
});

describe('toListenError', () => {
it('keeps the in-use wording for EADDRINUSE', () => {
const err = Object.assign(
new Error('listen EADDRINUSE: address already in use ::1:8000'),
{code: 'EADDRINUSE'},
);

const error = toListenError(err, 'localhost', 8000);

expect(error.message).toBe('Port 8000 is already in use');
expect(error.cause).toBe(err);
});

it('explains both causes of EACCES and how to work around them', () => {
const err = Object.assign(
new Error('listen EACCES: permission denied ::1:80'),
{code: 'EACCES'},
);

const error = toListenError(err, 'localhost', 80);

expect(error.message).toContain('localhost:80');
expect(error.message).toContain('below 1024');
expect(error.message).toContain(
'netsh interface ipv4 show excludedportrange protocol=tcp',
);
expect(error.message).toContain('port 0');
expect(error.message).not.toContain('--port');
expect(error.cause).toBe(err);
});

it('passes an unrelated errno through unchanged', () => {
const err = Object.assign(new Error('read ECONNRESET'), {
code: 'ECONNRESET',
});

expect(toListenError(err, 'localhost', 8000)).toBe(err);
});

it('passes an error with no code through unchanged', () => {
const err = new Error('something else went wrong');

expect(toListenError(err, 'localhost', 8000)).toBe(err);
});
});
Loading