nestjs/nest · error

An unsupported message was received. It has been negative ac

Error message

An unsupported message was received. It has been negative acknowledged, so it will not be re-delivered. Pattern: ${pattern}

What it means

ServerRMQ received an incoming request (a packet with an `id`, i.e. one expecting a reply) whose serialized pattern matches no registered @MessageHandler (getHandlerByPattern returned undefined). In manual-ack mode (noAck false, the default) the server negative-acknowledges the message with requeue=false — RabbitMQ drops it permanently — and logs this warning; the requesting client instead receives an error response carrying the constant NO_MESSAGE_HANDLER ('There is no matching message handler defined in the remote service.'). With noAck: true the nack and warning are skipped but the drop is equally silent.

Source

Thrown at packages/microservices/server/server-rmq.ts:313

    if (isNil(message)) {
      return;
    }
    const { content, properties } = message;
    const rawMessage = this.parseMessageContent(content);
    const packet = await this.deserializer.deserialize(rawMessage, properties);
    const pattern = isString(packet.pattern)
      ? packet.pattern
      : JSON.stringify(packet.pattern);

    const rmqContext = new RmqContext([message, channel, pattern]);
    if (isUndefined((packet as IncomingRequest).id)) {
      return this.handleEvent(pattern, packet, rmqContext);
    }
    const handler = this.getHandlerByPattern(pattern);

    if (!handler) {
      if (!this.noAck) {
        this.logger.warn(RMQ_NO_MESSAGE_HANDLER`${pattern}`);
        this.channel!.nack(rmqContext.getMessage() as Message, false, false);
      }
      const status = 'error';
      const noHandlerPacket = {
        id: (packet as IncomingRequest).id,
        err: NO_MESSAGE_HANDLER,
        status,
      };
      return this.sendMessage(
        noHandlerPacket,
        properties.replyTo,
        properties.correlationId,
        rmqContext,
      );
    }
    return this.onProcessingStartHook(
      this.transportId,
      rmqContext,

View on GitHub (pinned to dd75d7bd8c)

Solutions

  1. Make the pattern identical on both sides — exact string, or a shared object constant — and remember object patterns are matched by their JSON serialization, so key order matters.
  2. Give each microservice its own queue; a shared queue round-robins messages to instances that may not have the handler.
  3. Confirm the module containing the handlers is actually imported before producers go live (inspect registered patterns at startup).
  4. Configure a dead-letter exchange on the queue so nacked unsupported messages are captured for inspection instead of silently dropped, and purge stale messages after renaming/removing patterns.
  5. During rolling deploys, ship the consumer that handles both old and new patterns before switching producers.

Example fix

// before
// producer
this.client.send({ cmd: 'get-user' }, id);
// consumer
@MessageHandler({ cmd: 'get_user' }) // mismatch -> nack, message dropped

// after: one shared constant (shared package)
export const GET_USER = { cmd: 'get-user' } as const;
// producer
this.client.send(GET_USER, id);
// consumer
@MessageHandler(GET_USER)
getUser(@Payload() id: string) { /* ... */ }
Defensive patterns

Strategy: validation

Validate before calling

// share one source of truth for patterns and validate before publishing
export const PATTERNS = ['users.get', 'users.create'] as const;
export type Pattern = (typeof PATTERNS)[number];

function assertKnownPattern(p: string): asserts p is Pattern {
  if (!(PATTERNS as readonly string[]).includes(p)) {
    throw new Error(`Unknown pattern '${p}': server has no @MessageHandler for it`);
  }
}

assertKnownPattern(pattern);
this.client.send(pattern, payload);

Type guard

const PATTERNS = ['users.get', 'users.create'] as const;
type Pattern = (typeof PATTERNS)[number];
const isPattern = (p: string): p is Pattern =>
  (PATTERNS as readonly string[]).includes(p);

Try / catch

this.client.send<string, User>(pattern, payload).subscribe({
  next: (user) => this.logger.log(user),
  error: (err) => {
    // server replied with the NO_MESSAGE_HANDLER constant
    if (/no matching message handler/i.test(String(err?.message ?? err))) {
      // pattern contract is broken: alert and stop retrying, this is not transient
    }
  },
});

Prevention

When it happens

Trigger: client.send(pattern, payload) where the pattern does not exactly equal a @MessageHandler(pattern) on the server: typos, casing differences, or object patterns whose JSON.stringify key order differs (matching uses the serialized string). Also raw JSON published to the service's queue by external producers lacking the {id, pattern, data} contract, and version skew where a new producer ships before the consumer that handles the new pattern.

Common situations: Pattern typos between services; renaming a handler while old messages sit in the queue; multiple microservices configured with the same queue so messages round-robin to an instance that never registered the pattern; rolling deploys with old and new pattern names; third-party systems publishing directly to the queue.

Related errors


AI-assisted analysis of nestjs/nest@dd75d7bd8c (2026-08-21). Data as JSON: /api/errors/b466079baa4b1e90. Report an issue: GitHub.