diff --git a/src/cli/nso/http-server.ts b/src/cli/nso/http-server.ts index 0eec540..d9139f2 100644 --- a/src/cli/nso/http-server.ts +++ b/src/cli/nso/http-server.ts @@ -755,10 +755,7 @@ class Server extends HttpServer { this.resetAuthTimeout(na_session_token, () => user.data.user.id); } } catch (err) { - stream.sendEvent('error', { - error: (err as Error).name, - error_message: (err as Error).message, - }); + stream.sendErrorEvent(err); } } } diff --git a/src/cli/presence-server.ts b/src/cli/presence-server.ts index 7295a48..9747579 100644 --- a/src/cli/presence-server.ts +++ b/src/cli/presence-server.ts @@ -1095,8 +1095,9 @@ class Server extends HttpServer { if (retry_after && /^\d+$/.test(retry_after)) { stream.sendEvent(null, 'debug: timestamp ' + new Date().toISOString(), { - error: err, + error: 'unknown_error', error_message: (err as Error).message, + ...err, }); await new Promise(rs => setTimeout(rs, parseInt(retry_after) * 1000)); @@ -1105,17 +1106,7 @@ class Server extends HttpServer { } } - if (err instanceof ResponseError) { - stream.sendEvent('error', { - error: err.code, - error_message: err.message, - }); - } else { - stream.sendEvent('error', { - error: err, - error_message: (err as Error).message, - }); - } + stream.sendErrorEvent(err); debug('Error in event stream %d', stream.id, err); diff --git a/src/cli/util/http-server.ts b/src/cli/util/http-server.ts index 5d05cd1..f66d00f 100644 --- a/src/cli/util/http-server.ts +++ b/src/cli/util/http-server.ts @@ -71,10 +71,7 @@ export class HttpServer { if (err instanceof ResponseError) { err.sendResponse(req, res); } else { - this.sendJsonResponse(res, { - error: err, - error_message: (err as Error).message, - }, 500); + this.sendJsonResponse(res, getErrorObject(err), 500); } } } @@ -85,16 +82,24 @@ export class ResponseError extends Error { } sendResponse(req: Request, res: Response) { - const data = { - error: this.code, - error_message: this.message, - }; + const data = this.toJSON(); res.statusCode = this.status; res.setHeader('Content-Type', 'application/json'); res.end(req.headers['accept']?.match(/\/html\b/i) ? JSON.stringify(data, null, 4) : JSON.stringify(data)); } + + sendEventStreamEvent(events: EventStreamResponse) { + events.sendEvent('error', this.toJSON()); + } + + toJSON() { + return { + error: this.code, + error_message: this.message, + }; + } } export class EventStreamResponse { @@ -123,8 +128,96 @@ export class EventStreamResponse { sendEvent(event: string | null, ...data: unknown[]) { if (event) this.res.write('event: ' + event + '\n'); - for (const d of data) this.res.write('data: ' + JSON.stringify(d, - this.json_replacer ? (k, v) => this.json_replacer?.call(null, k, v, d) : undefined) + '\n'); + for (const d of data) { + if (d instanceof EventStreamField) d.write(this.res); + else this.res.write('data: ' + JSON.stringify(d, + this.json_replacer ? (k, v) => this.json_replacer?.call(null, k, v, d) : undefined) + '\n'); + } this.res.write('\n'); } + + sendErrorEvent(err: unknown) { + if (err instanceof ResponseError) { + err.sendEventStreamEvent(this); + } else { + this.sendEvent('error', getErrorObject(err)); + } + } +} + +abstract class EventStreamField { + abstract write(res: Response): void; +} + +export class EventStreamLastEventId extends EventStreamField { + constructor( + readonly id: string, + ) { + super(); + + if (!/^[0-9a-z-_\.:;]+$/i.test(id)) { + throw new TypeError('Invalid event ID'); + } + } + + write(res: Response) { + res.write('id: ' + this.id + '\n'); + } +} + +export class EventStreamRetryTime extends EventStreamField { + constructor( + readonly retry_ms: number, + ) { + super(); + } + + write(res: Response) { + res.write('retry: ' + this.retry_ms + '\n'); + } +} + +export class EventStreamRawData extends EventStreamField { + constructor( + readonly data: string, + ) { + super(); + + if (/[\0\n\r]/.test(data)) { + throw new TypeError('Invalid data'); + } + } + + write(res: Response) { + res.write('data: ' + this.data + '\n'); + } +} + +function getErrorObject(err: unknown) { + if (err instanceof ResponseError) { + return err.toJSON(); + } + + if (err && typeof err === 'object' && 'type' in err && 'code' in err && 'message' in err && err.type === 'system') { + return { + error: 'unknown_error', + error_message: err.message, + error_code: err.code, + ...err, + }; + } + + if (err instanceof Error) { + return { + error: 'unknown_error', + error_message: err.message, + ...err, + }; + } + + return { + error: 'unknown_error', + error_message: (err as Error)?.message, + ...(err as object), + }; }