Skip to content

Unhandled Temporal.Duration.from() error in NOTIFY callback crashes the process #594

@dahlia

Description

@dahlia

Description

In PostgresMessageQueue.listen(), the NOTIFY callback passed to this.#sql.listen() calls Temporal.Duration.from(delay) without any error handling. If the NOTIFY payload is malformed (e.g., empty string or invalid duration format), the resulting RangeError propagates through the postgres driver's NotificationResponse handler as an unhandled promise rejection, crashing the entire process.

Relevant code

https://github.com/fedify-dev/fedify/blob/2.0-maintenance/packages/postgres/src/mq.ts#L338-L353

const listen = await this.#sql.listen(
  this.#channelName,
  async (delay) => {
    const duration = Temporal.Duration.from(delay);  // ← No try-catch
    const durationMs = duration.total("millisecond");
    if (durationMs < 1) await safeSerializedPoll("notify-immediate");
    else {
      const timeout = setTimeout(() => {
        timeouts.delete(timeout);
        void safeSerializedPoll("notify-delayed");
      }, durationMs);
      timeouts.add(timeout);
    }
  },
  () => safeSerializedPoll("subscribe"),
);

Note that while the poll() errors are handled by safeSerializedPoll(), the Temporal.Duration.from() call happens before safeSerializedPoll() is reached, so the error is completely unguarded.

Error observed in production

Uncaught (in promise) RangeError: Temporal error: Parsing ended abruptly.
    at NotificationResponse (postgres/src/connection.js:814:5)
    at handle (postgres/src/connection.js:480:6)
    at Socket.data (postgres/src/connection.js:315:9)
    at Socket.emit (ext:deno_node/_events.mjs:436:20)
    at addChunk (node:_stream_readable:452:12)

All three application instances crashed simultaneously when a malformed NOTIFY payload was received on the fedify_channel.

Suggested fix

Wrap the NOTIFY callback body in a try–catch:

async (delay) => {
  try {
    const duration = Temporal.Duration.from(delay);
    const durationMs = duration.total("millisecond");
    if (durationMs < 1) await safeSerializedPoll("notify-immediate");
    else {
      const timeout = setTimeout(() => {
        timeouts.delete(timeout);
        void safeSerializedPoll("notify-delayed");
      }, durationMs);
      timeouts.add(timeout);
    }
  } catch (error) {
    logger.error(
      "Error parsing NOTIFY payload {delay}: {error}",
      { delay, error },
    );
    // Fall back to an immediate poll
    await safeSerializedPoll("notify-fallback");
  }
},

Environment

  • @fedify/postgres used with Fedify 2.0.2
  • PostgreSQL 17
  • Deno runtime
  • Multiple application instances sharing the same database

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions