apache/beam · error · IllegalStateException

Can't set both the topic and the subscription for a PubsubIO

Error message

Can't set both the topic and the subscription for a PubsubIO.Read transform

What it means

Read.expand() enforces mutually exclusive sources: a PubsubIO.Read transform may read from a topic OR a subscription, never both. When both are set, this IllegalStateException is thrown at pipeline construction.

Source

Thrown at sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java:1228

    @VisibleForTesting
    /**
     * Set's the internal Clock.
     *
     * <p>Only for use by unit tests.
     */
    Read<T> withClock(Clock clock) {
      return toBuilder().setClock(clock).build();
    }

    @Override
    public PCollection<T> expand(PBegin input) {
      if (getTopicProvider() == null && getSubscriptionProvider() == null) {
        throw new IllegalStateException(
            "Need to set either the topic or the subscription for " + "a PubsubIO.Read transform");
      }
      if (getTopicProvider() != null && getSubscriptionProvider() != null) {
        throw new IllegalStateException(
            "Can't set both the topic and the subscription for " + "a PubsubIO.Read transform");
      }

      if (getDeadLetterTopicProvider() != null
          && !(getBadRecordRouter() instanceof ThrowingBadRecordRouter)) {
        throw new IllegalArgumentException(
            "PubSubIO cannot be configured with both a dead letter topic and a bad record router");
      }

      ValueProvider<PubsubTopic> topicProvider = getTopicProvider();
      @Nullable ValueProvider<TopicPath> topicPath =
          topicProvider == null
              ? null
              : NestedValueProvider.of(topicProvider, new TopicPathTranslator());
      ValueProvider<PubsubSubscription> subscriptionProvider = getSubscriptionProvider();
      @Nullable ValueProvider<SubscriptionPath> subscriptionPath =
          subscriptionProvider == null
              ? null

View on GitHub (pinned to 12126d8942)

Solutions

  1. Set only one: keep fromSubscription(...) if you want a durable subscription, and remove the fromTopic(...) call.
  2. If both values exist in config, choose with an if/else instead of setting both.
  3. Note topic-vs-subscription semantics differ (subscription tracks acks); pick deliberately.

Example fix

// before
PubsubIO.readStrings().fromTopic(t).fromSubscription(s);
// after
PubsubIO.readStrings().fromSubscription(s); // or fromTopic(t), never both
Defensive patterns

Strategy: validation

Validate before calling

checkArgument(topic == null || subscription == null,
    "Set only one of topic or subscription, not both");
PubsubIO.Read<String> read = subscription != null
    ? PubsubIO.readStrings().fromSubscription(subscription)
    : PubsubIO.readStrings().fromTopic(topic);

Type guard

boolean exactlyOneSource(String topic, String subscription) {
  return (topic != null) ^ (subscription != null);
}

Try / catch

try {
  return pipeline.apply(read);
} catch (IllegalStateException e) {
  if (e.getMessage().contains("Can't set both the topic and the subscription"))
    throw new ConfigException("Provide either topic or subscription, not both", e);
  throw e;
}

Prevention

When it happens

Trigger: Calling both fromTopic(...) and fromSubscription(...) on the same Read transform — commonly when merging config values or chaining builder calls where each branch sets a different source.

Common situations: Config providing both topic and subscription keys and code setting both unconditionally; combining a default builder (with a topic) with a subscription override.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/524886962c5f3ff3. Report an issue: GitHub.