Contain a throw from the application's logger instead of letting it escape

This commit is contained in:
2026-09-04 11:33:20 +02:00
parent 80530c6265
commit 2e422c7584
5 changed files with 51 additions and 10 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ import { checkSessionOptions, undeclaredInterfaceVersion } from './session-optio
import { connect as netConnect } from 'node:net';
import { connect as tlsConnect } from 'node:tls';
import { defaultInterfaceVersion } from './defs/constants.ts';
import { silentLog } from './log.ts';
import { guardedLog } from './log.ts';
export type ClientOptions = {
addressRange?: string;
@@ -194,7 +194,7 @@ async function connect(options: ClientOptions, log: SmppLog): Promise<Result<{ s
/** Connects to an SMSC and binds. */
export async function client(options: ClientOptions = {}): Promise<Result<{ session: Session }>> {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
const opened = await connect(options, log);
if (opened.err) return { err: opened.err };
+23
View File
@@ -20,3 +20,26 @@ export const silentLog: SmppLog = {
verbose: noop,
warn: noop,
};
function contained(log: SmppLog, method: keyof SmppLog): LogMethod {
return (msg, metadata) => {
try {
log[method](msg, metadata);
} catch {
// Reporting a broken logger would need a logger, and hard rule 1 outranks the report.
}
};
}
/** The application's logger with a throw from it contained, or silence when it supplied none. */
export function guardedLog(log?: SmppLog): SmppLog {
if (log === undefined) return silentLog;
return {
debug: contained(log, 'debug'),
error: contained(log, 'error'),
info: contained(log, 'info'),
verbose: contained(log, 'verbose'),
warn: contained(log, 'warn'),
};
}
+6 -6
View File
@@ -12,7 +12,7 @@ import { createServer as createTlsServer } from 'node:tls';
import { defaultInterfaceVersion } from './defs/constants.ts';
import { errorFrom } from './error-from.ts';
import { paramText } from './defs/types.ts';
import { silentLog } from './log.ts';
import { guardedLog } from './log.ts';
export type AuthenticateResult = { userData?: unknown } | boolean;
@@ -185,7 +185,7 @@ async function acceptBind(
}
async function onBind(session: Session, pduObj: PduObject, options: ServerOptions): Promise<void> {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
const identity = { system_id: options.systemId ?? defaults.systemId };
const systemId = paramText(pduObj.params.system_id);
@@ -212,7 +212,7 @@ async function onRequest(
if (session.loggedIn || pduObj.cmdName === 'unbind') return false;
if (!bindCommands.includes(pduObj.cmdName)) {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
log.debug('server - command before bind', { cmdName: pduObj.cmdName });
await session.sendReturn(pduObj, 'ESME_RINVBNDSTS');
@@ -226,7 +226,7 @@ async function onRequest(
}
function onConnection(sock: Socket, options: ServerOptions, server: SmppServer): void {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
const session = new Session({
idleTimeout: options.idleTimeout ?? defaults.idleTimeout,
log,
@@ -304,7 +304,7 @@ function createListener(
}
function onListening(listener: NetServer, smpp: SmppServer, options: ServerOptions): void {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
// Past startup, a listener error is a runtime event, not a failed start.
listener.on('error', (err: Error) => {
@@ -321,7 +321,7 @@ function onListening(listener: NetServer, smpp: SmppServer, options: ServerOptio
/** Starts listening for SMPP connections. Resolves once the socket is bound. */
export function server(options: ServerOptions = {}): Promise<Result<{ server: SmppServer }>> {
const log = options.log ?? silentLog;
const log = guardedLog(options.log);
const port = options.port ?? defaults.port;
const created = createListener(options, log, port);
+2 -2
View File
@@ -19,7 +19,7 @@ import { errorFrom } from './error-from.ts';
import { optionalParamsMinVersion } from './defs/constants.ts';
import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts';
import { isResp, pduReturn } from './pdu.ts';
import { silentLog } from './log.ts';
import { guardedLog } from './log.ts';
import { submitSms, unsent } from './send-sms.ts';
import { ConcatReference } from './udh.ts';
@@ -106,7 +106,7 @@ export class Session extends EventEmitter<SessionEvents> {
constructor(options: SessionOptions) {
super({ captureRejections: true });
this.log = options.log ?? silentLog;
this.log = guardedLog(options.log);
this.options = options;
this.dlrMerger = new DlrMerger({
log: this.log,
+18
View File
@@ -1329,6 +1329,24 @@ describe('application hooks that throw or reject', () => {
assert.equal(smpp.sessions.size, 0);
});
test('sends on through an application logger that throws', async t => {
const thrower = (): void => { throw new Error('the logger exploded'); };
const log: SmppLog = { debug: thrower, error: thrower, info: thrower, verbose: thrower, warn: thrower };
const smpp = await startServer(t, { log });
smpp.on('session', session => {
session.on('sms', sms => { void sms.sendResp(); });
});
const { session } = await connect(t, smpp, { log });
assert.ok(session);
const sent = await session.sendSms({ from: '46701113311', message: 'logged', to: '46709771337' });
assert.equal(sent.err, undefined);
});
test('refuses a send window that can never free a slot', async t => {
const smpp = await startServer(t);
const { err, session } = await connect(t, smpp, { maxOutstanding: 0 });