{"record":{"id":"7d8ef56d081b96c1","repo":"apache/beam","slug":"pubsubio-cannot-be-configured-with-both-a-dead-letter-topic","errorCode":null,"errorMessage":"PubSubIO cannot be configured with both a dead letter topic and a bad record router","messagePattern":"PubSubIO cannot be configured with both a dead letter topic and a bad record router","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java","lineNumber":1234,"sourceCode":"     */\n    Read<T> withClock(Clock clock) {\n      return toBuilder().setClock(clock).build();\n    }\n\n    @Override\n    public PCollection<T> expand(PBegin input) {\n      if (getTopicProvider() == null && getSubscriptionProvider() == null) {\n        throw new IllegalStateException(\n            \"Need to set either the topic or the subscription for \" + \"a PubsubIO.Read transform\");\n      }\n      if (getTopicProvider() != null && getSubscriptionProvider() != null) {\n        throw new IllegalStateException(\n            \"Can't set both the topic and the subscription for \" + \"a PubsubIO.Read transform\");\n      }\n\n      if (getDeadLetterTopicProvider() != null\n          && !(getBadRecordRouter() instanceof ThrowingBadRecordRouter)) {\n        throw new IllegalArgumentException(\n            \"PubSubIO cannot be configured with both a dead letter topic and a bad record router\");\n      }\n\n      ValueProvider<PubsubTopic> topicProvider = getTopicProvider();\n      @Nullable ValueProvider<TopicPath> topicPath =\n          topicProvider == null\n              ? null\n              : NestedValueProvider.of(topicProvider, new TopicPathTranslator());\n      ValueProvider<PubsubSubscription> subscriptionProvider = getSubscriptionProvider();\n      @Nullable ValueProvider<SubscriptionPath> subscriptionPath =\n          subscriptionProvider == null\n              ? null\n              : NestedValueProvider.of(subscriptionProvider, new SubscriptionPathTranslator());\n      PubsubUnboundedSource source =\n          new PubsubUnboundedSource(\n              getClock(),\n              getPubsubClientFactory(),\n              null /* always get project from runtime PipelineOptions */,","sourceCodeStart":1216,"sourceCodeEnd":1252,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java#L1216-L1252","documentation":"PubsubIO.Read rejects a configuration that sets both a dead letter topic and a custom BadRecordRouter. These two failure-handling mechanisms are mutually exclusive: the dead letter topic is itself implemented via the bad-record routing path, so an explicit router would conflict with dead-letter delivery. The library throws IllegalArgumentException at pipeline construction time in Read.expand() before any data is processed.","triggerScenarios":"Calling PubsubIO.readMessages()...withDeadLetterTopic(...) while a non-ThrowingBadRecordRouter bad record router is also set (e.g. via withBadRecordRouter(...)), then expanding the transform.","commonSituations":"Developers copying a pipeline that used a custom bad record router and adding a dead letter topic for durability, or chaining configuration helpers that each set one of the two options.","solutions":["Remove the withBadRecordRouter(...) call and keep withDeadLetterTopic(...) if you want failed records sent to the DLQ.","Remove withDeadLetterTopic(...) and keep the custom bad record router if you need custom routing behavior.","If you need both behaviors, implement the dead-letter logic inside your custom BadRecordRouter instead."],"exampleFix":"// before\nPubsubIO.readMessages()\n    .withDeadLetterTopic(topic)\n    .withBadRecordRouter(myRouter);\n// after\nPubsubIO.readMessages()\n    .withDeadLetterTopic(topic); // default ThrowingBadRecordRouter","handlingStrategy":"validation","validationCode":"// before expanding the transform\nif (io.getDeadLetterTopicProvider() != null && !(io.getBadRecordRouter() instanceof ThrowingBadRecordRouter)) {\n  throw new IllegalArgumentException(\"Choose either deadLetterTopic or a custom BadRecordRouter, not both\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pick one failure-handling strategy (DLQ vs custom router) per PubsubIO transform at design time.","Centralize PubsubIO builder configuration in one helper so conflicting options can't be combined.","Run pipeline construction in unit tests (expand only) to catch config conflicts before launch."],"tags":["java","pubsub","configuration","pipeline-construction"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}