{"record":{"id":"626f87392ecd37d9","repo":"microsoft/semantic-kernel","slug":"message-must-have-a-topic-to-be-published","errorCode":null,"errorMessage":"Message must have a topic to be published.","messagePattern":"Message must have a topic to be published\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs","lineNumber":356,"sourceCode":"                continue;\n            }\n\n            Task actualTask = processTask.AsTask();\n            pendingTasks.Add(taskId, actualTask.ContinueWith(t => pendingTasks.Remove(taskId), TaskScheduler.Current));\n        }\n\n        // The pending task dictionary may contain null values when a race condition is experienced during\n        // the prior \"ContinueWith\" call.  This could be solved with a ConcurrentDictionary, but locking\n        // is entirely undesirable in this context.\n        await Task.WhenAll([.. pendingTasks.Values.Where(task => task is not null)]).ConfigureAwait(false);\n        await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false);\n    }\n\n    private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken)\n    {\n        if (!envelope.Topic.HasValue)\n        {\n            throw new InvalidOperationException(\"Message must have a topic to be published.\");\n        }\n\n        List<Task>? tasks = null;\n        TopicId topic = envelope.Topic.Value;\n        foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic)))\n        {\n            (tasks ??= []).Add(ProcessSubscriptionAsync(envelope, topic, subscription, deliveryToken));\n        }\n\n        if (tasks is not null)\n        {\n            await Task.WhenAll(tasks).ConfigureAwait(false);\n        }\n\n        async Task ProcessSubscriptionAsync(MessageEnvelope envelope, TopicId topic, ISubscriptionDefinition subscription, CancellationToken deliveryToken)\n        {\n            deliveryToken.ThrowIfCancellationRequested();\n","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/microsoft/semantic-kernel/blob/c028a0c7dc4f0814cdcbaba9d998f187a41197bf/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs#L338-L374","documentation":"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.","triggerScenarios":"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.","commonSituations":"Subclassing or extending the runtime and reusing the servicer incorrectly; bugs in custom message-delivery plumbing; envelope mutation between enqueue and service.","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."],"exampleFix":"// before (custom plumbing)\nvar env = new MessageEnvelope(msg, id, ct); // no topic\nenv.ForPublish(default(TopicId), publishServicer); // topic never set\n\n// after\nawait runtime.PublishMessageAsync(msg, new TopicId(\"myTopic\"));","handlingStrategy":"validation","validationCode":"// Always publish through the public API which sets the topic.\nawait runtime.PublishMessageAsync(message, new TopicId(\"myTopic\"));\n// If building an envelope manually, ensure a TopicId is supplied to ForPublish.","typeGuard":"bool HasTopic(MessageEnvelope env) => env.Topic.HasValue;","tryCatchPattern":null,"preventionTips":["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."],"tags":["inprocess-runtime","messaging","publish","topic","dotnet"],"backgroundTag":null,"analyzedSha":"c028a0c7dc4f0814cdcbaba9d998f187a41197bf","analyzedAt":"2026-08-13T13:48:05.040Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}