apache/beam · error

Exactly one of Topic or Subscription must be set in…

Error message

Exactly one of Topic or Subscription must be set in ReadOptions

What it means

pubsubio.Read requires ReadOptions to set exactly one of Topic or Subscription. The library panics when both are set (ambiguous source) or neither is set (no source). Validation happens synchronously during pipeline construction.

Solutions

  1. Set exactly one field: use Topic to read from a topic, or Subscription for an existing subscription.
  2. If both are populated from config, add logic to prefer/clear one before calling Read.
  3. Guard with a validation check on the options struct before constructing the pipeline.

Example fix

// before
pubsubio.Read(s, project, pubsubio.ReadOptions{Topic: "t", Subscription: "sub"})
// after
pubsubio.Read(s, project, pubsubio.ReadOptions{Subscription: "sub"})
Defensive patterns

Strategy: validation

Validate before calling

func validateReadOptions(o pubsubio.ReadOptions) error {
    if (o.Topic == "") == (o.Subscription == "") {
        return errors.New("set exactly one of Topic or Subscription")
    }
    return nil
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        if s, ok := r.(string); ok && strings.Contains(s, "Exactly one of Topic or Subscription") {
            // handle
        } else { panic(r) }
    }
}()

Prevention

When it happens

Trigger: Calling pubsubio.Read(s, project, pubsubio.ReadOptions{}) with both Topic and Subscription populated, or with both left empty.

Common situations: Copy-pasting ReadOptions between read and write code leaving stale fields; building options from flags where one flag was renamed and never wired; templated pipelines that fill both fields unconditionally.

Related errors


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

Appendix: source

Thrown at sdks/go/pkg/beam/io/pubsubio/pubsubio.go:72

type ReadOptions struct {
	Topic              string // Topic sets the topic to be read from. A new subscription will be generated for the job. Mutually exclusive with setting a Subscription.
	Subscription       string // Subscription sets the name of an existing subscription to read from. Mutually exclusive with setting a Topic.
	IDAttribute        string
	TimestampAttribute string
	WithAttributes     bool
}

// Read reads an unbounded number of PubSubMessages from the given
// pubsub topic or subscription. It produces an unbounded PCollecton<*PubSubMessage>,
// if WithAttributes is set, or an unbounded PCollection<[]byte>.
//
// The topic or subscription is required and must be set with ReadOptions.
func Read(s beam.Scope, project string, opts ReadOptions) beam.PCollection {
	s = s.Scope("pubsubio.Read")

	// Validate: only one of Topic or Subscription should be set
	if (opts.Topic == "" && opts.Subscription == "") || (opts.Topic != "" && opts.Subscription != "") {
		panic("Exactly one of Topic or Subscription must be set in ReadOptions")
	}

	payload := &pipepb.PubSubReadPayload{}

	if opts.Topic != "" {
		payload.Topic = pubsubx.MakeQualifiedTopicName(project, opts.Topic)
	} else {
		payload.Subscription = pubsubx.MakeQualifiedSubscriptionName(project, opts.Subscription)
	}

	payload.IdAttribute = opts.IDAttribute
	payload.TimestampAttribute = opts.TimestampAttribute
	payload.WithAttributes = opts.WithAttributes

	out := beam.External(
		s,
		readURN,
		protox.MustEncode(payload),

View on GitHub (pinned to 12126d8942)