microsoft/semantic-kernel · error · InvalidOperationException
Message must have a topic to be published.
Error message
Message must have a topic to be published.
What it means
PublishMessageServicerAsync (the internal servicer wired by ForPublish) throws InvalidOperationException if envelope.Topic has no value. The public PublishMessageAsync always sets the topic via ForPublish, so reaching this throw requires a MessageEnvelope constructed without a topic being routed through the publish servicer — typically custom/internal misuse rather than normal API usage.
Source
Thrown at dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs:356
continue;
}
Task actualTask = processTask.AsTask();
pendingTasks.Add(taskId, actualTask.ContinueWith(t => pendingTasks.Remove(taskId), TaskScheduler.Current));
}
// The pending task dictionary may contain null values when a race condition is experienced during
// the prior "ContinueWith" call. This could be solved with a ConcurrentDictionary, but locking
// is entirely undesirable in this context.
await Task.WhenAll([.. pendingTasks.Values.Where(task => task is not null)]).ConfigureAwait(false);
await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
}
private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken)
{
if (!envelope.Topic.HasValue)
{
throw new InvalidOperationException("Message must have a topic to be published.");
}
List<Task>? tasks = null;
TopicId topic = envelope.Topic.Value;
foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic)))
{
(tasks ??= []).Add(ProcessSubscriptionAsync(envelope, topic, subscription, deliveryToken));
}
if (tasks is not null)
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
async Task ProcessSubscriptionAsync(MessageEnvelope envelope, TopicId topic, ISubscriptionDefinition subscription, CancellationToken deliveryToken)
{
deliveryToken.ThrowIfCancellationRequested();
View on GitHub (pinned to c028a0c7dc)
Solutions
- Always publish via the public PublishMessageAsync(message, topic, ...) which guarantees the topic is set.
- If building a MessageEnvelope manually for publish, always call WithSender/ForPublish with a valid TopicId.
- Do not reuse the send servicer for publish envelopes or vice versa.
Example fix
// before (custom plumbing)
var env = new MessageEnvelope(msg, id, ct); // no topic
env.ForPublish(default(TopicId), publishServicer); // topic never set
// after
await runtime.PublishMessageAsync(msg, new TopicId("myTopic")); Defensive patterns
Strategy: validation
Validate before calling
// Always publish through the public API which sets the topic.
await runtime.PublishMessageAsync(message, new TopicId("myTopic"));
// If building an envelope manually, ensure a TopicId is supplied to ForPublish. Type guard
bool HasTopic(MessageEnvelope env) => env.Topic.HasValue;
Prevention
- Use PublishMessageAsync rather than wiring servicers manually.
- Never route a send-oriented envelope through the publish servicer.
- When extending the runtime, preserve the topic through to the servicer.
When it happens
Trigger: Constructing a MessageEnvelope and calling ForPublish without a topic; a custom runtime/servicer reusing PublishMessageServicerAsync on an envelope built for send; internal code path that drops the topic before servicing.
Common situations: Subclassing or extending the runtime and reusing the servicer incorrectly; bugs in custom message-delivery plumbing; envelope mutation between enqueue and service.
Related errors
- Delivery of message {messageId} was cancelled.
- Message must have a receiver to be sent.
- TopicId does not match the subscription.
- TopicId does not match the subscription.
- Runtime is already running.
AI-assisted analysis of microsoft/semantic-kernel@c028a0c7dc (2026-08-13).
Data as JSON: /api/errors/626f87392ecd37d9.
Report an issue: GitHub.