Skip to content

feat(trogon-source-linear): publish unroutable payloads to DLQ#97

Merged
yordis merged 1 commit intomainfrom
yordis/linear-webhook-fields
Apr 5, 2026
Merged

feat(trogon-source-linear): publish unroutable payloads to DLQ#97
yordis merged 1 commit intomainfrom
yordis/linear-webhook-fields

Conversation

@yordis
Copy link
Copy Markdown
Member

@yordis yordis commented Apr 5, 2026

Summary

  • Authenticated webhook payloads that fail validation were silently discarded with a 400 — no way to inspect or debug what Linear sent
  • Mirrors the {prefix}.unroutable DLQ pattern already used by trogon-source-slack, publishing rejected payloads to linear.unroutable with an X-Linear-Reject-Reason header
  • Auth failures (missing/invalid signature) still return 401 without DLQ since untrusted payloads should not be stored

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 5, 2026

PR Summary

Medium Risk
Changes webhook validation behavior to publish otherwise-rejected (but authenticated) payloads into JetStream, which could affect message volume and downstream consumers if validation is overly strict or misclassified. Auth-failure paths remain unchanged (401 without storing untrusted payloads).

Overview
Authenticated Linear webhook payloads that fail validation are now published to a {LINEAR_SUBJECT_PREFIX}.unroutable subject (DLQ) with an X-Linear-Reject-Reason header indicating why the payload was rejected (e.g. invalid JSON, missing/invalid type/action, missing/stale timestamp).

This introduces a publish_unroutable path in the webhook handler, adds the reject-reason constant, updates crate docs to describe the DLQ behavior, and adds unit tests (with new dev-deps like tower, tracing-subscriber, and trogon-nats test support) covering success, rejection, and publish-failure cases.

Reviewed by Cursor Bugbot for commit 65d9885. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfef8080-d234-4753-bbf8-2fc5d4b8c725

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac20b7 and 65d9885.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • rsworkspace/crates/trogon-source-linear/Cargo.toml
  • rsworkspace/crates/trogon-source-linear/src/constants.rs
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
  • rsworkspace/crates/trogon-source-linear/src/server.rs

Walkthrough

The changes extend the Linear webhook handler to route validation failures to a dedicated NATS subject. A new constant defines the rejection reason header, crate documentation describes the new routing behavior for unroutable messages, and server logic adds a publish helper and validation failure handlers.

Changes

Cohort / File(s) Summary
NATS Header Configuration
rsworkspace/crates/trogon-source-linear/src/constants.rs
Added NATS_HEADER_REJECT_REASON constant with value "X-Linear-Reject-Reason" for header identification.
Documentation
rsworkspace/crates/trogon-source-linear/src/lib.rs
Updated crate-level docs to describe webhook validation failure routing: unverifiable or invalid payloads are published to .unroutable subject with rejection reason header.
Webhook Validation & Publishing
rsworkspace/crates/trogon-source-linear/src/server.rs
Added publish_unroutable(...) helper function and integrated validation failure handlers for JSON parsing, timestamp validation, and event type/action validation, all routing to NATS with rejection reason headers.

Sequence Diagram

sequenceDiagram
    participant Client as Webhook Client
    participant Server as Linear Handler
    participant NATS as NATS Stream

    Client->>Server: POST webhook payload
    
    alt Signature Valid
        alt Validation Passes
            Server->>NATS: Publish to linear.event
            NATS-->>Server: Ack
            Server-->>Client: 200 OK
        else Validation Fails
            rect rgba(220, 53, 69, 0.5)
                Note over Server: Invalid JSON, stale timestamp,<br/>missing/invalid type or action
                Server->>NATS: publish_unroutable(body, reason)
                NATS-->>Server: Ack
                Server-->>Client: 400 BAD_REQUEST
            end
        end
    else Signature Invalid
        Server-->>Client: 401 UNAUTHORIZED
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through Linear's way,
Routing lost messages with care,
With headers tagged and reasons shown,
Unroutable dreams find home,
In NATS streams, clear and fair!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding DLQ publishing for unroutable Linear webhook payloads, matching the primary feature introduced across the three files.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation, implementation pattern (DLQ with rejection headers), and security boundary (auth failures excluded).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/linear-webhook-fields

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

badge

Code Coverage Summary

Details
Filename                                                                      Stmts    Miss  Cover    Missing
--------------------------------------------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------
crates/acp-nats-stdio/src/config.rs                                              72       0  100.00%
crates/acp-nats-stdio/src/main.rs                                               141      27  80.85%   62, 114-121, 127-129, 146, 177-198
crates/trogon-source-linear/src/config.rs                                       208       0  100.00%
crates/trogon-source-linear/src/main.rs                                           4       0  100.00%
crates/trogon-source-linear/src/signature.rs                                     54       1  98.15%   16
crates/trogon-source-linear/src/server.rs                                       351       3  99.15%   175-177
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                   15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                 15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs       15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs               15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                    15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs           15       0  100.00%
crates/trogon-nats/src/token.rs                                                   8       0  100.00%
crates/trogon-nats/src/connect.rs                                               105      11  89.52%   22-24, 37, 49, 68-73
crates/trogon-nats/src/client.rs                                                 25      25  0.00%    50-89
crates/trogon-nats/src/auth.rs                                                  119       0  100.00%
crates/trogon-nats/src/messaging.rs                                             552       2  99.64%   132, 142
crates/trogon-nats/src/mocks.rs                                                 304       0  100.00%
crates/trogon-nats/src/nats_token.rs                                            161       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                        194       4  97.94%   254-256, 266
crates/acp-nats/src/jetstream/ext_policy.rs                                      26       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                       61       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                       99       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                          20       0  100.00%
crates/acp-nats/src/nats/subjects/responses/update.rs                            27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                         15       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                         18       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                   27       0  100.00%
crates/trogon-nats/src/jetstream/publish.rs                                      57       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                       474       0  100.00%
crates/trogon-source-slack/src/config.rs                                        163       0  100.00%
crates/trogon-source-slack/src/main.rs                                            4       0  100.00%
crates/trogon-source-slack/src/signature.rs                                      80       0  100.00%
crates/trogon-source-slack/src/server.rs                                        925       3  99.68%   119-121
crates/acp-nats/src/telemetry/metrics.rs                                         65       0  100.00%
crates/trogon-std/src/args.rs                                                    10       0  100.00%
crates/trogon-std/src/json.rs                                                    30       0  100.00%
crates/trogon-std/src/time/system.rs                                             27       3  88.89%   27-29
crates/trogon-std/src/time/mock.rs                                              129       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                           81       0  100.00%
crates/trogon-std/src/env/system.rs                                              17       0  100.00%
crates/acp-telemetry/src/metric.rs                                               35       4  88.57%   30-31, 38-39
crates/acp-telemetry/src/service_name.rs                                         31       0  100.00%
crates/acp-telemetry/src/signal.rs                                                3       3  0.00%    4-43
crates/acp-telemetry/src/lib.rs                                                 153      22  85.62%   39-46, 81, 86, 91, 105-120
crates/acp-telemetry/src/log.rs                                                  70       2  97.14%   39-40
crates/acp-telemetry/src/trace.rs                                                32       4  87.50%   23-24, 31-32
crates/acp-nats/src/client_proxy.rs                                             200       0  100.00%
crates/acp-nats/src/error.rs                                                     84       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                      32       0  100.00%
crates/acp-nats/src/req_id.rs                                                    39       0  100.00%
crates/acp-nats/src/config.rs                                                   204       0  100.00%
crates/acp-nats/src/lib.rs                                                       73       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                   141       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                           70       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                51       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                    6       0  100.00%
crates/acp-nats/src/session_id.rs                                                72       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                            8       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                           8       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                  12       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                8       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                            396       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                          71       0  100.00%
crates/acp-nats/src/client/session_update.rs                                     55       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                   294       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                       157       0  100.00%
crates/acp-nats/src/client/fs_read_text_file.rs                                 384       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                     309       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                451       0  100.00%
crates/acp-nats/src/client/mod.rs                                              2987       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                338       0  100.00%
crates/acp-nats/src/client/ext.rs                                               365       8  97.81%   193-204, 229-240
crates/acp-nats/src/client/terminal_output.rs                                   223       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                  357       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                              84       0  100.00%
crates/trogon-std/src/dirs/system.rs                                             98      11  88.78%   57, 65, 67, 75, 77, 85, 87, 96, 98, 109, 154
crates/acp-nats/src/nats/subjects/commands/close.rs                              18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                  18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                           18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                          18       0  100.00%
crates/trogon-source-github/src/server.rs                                       368       0  100.00%
crates/trogon-source-github/src/main.rs                                           4       0  100.00%
crates/trogon-source-github/src/config.rs                                       104       0  100.00%
crates/trogon-source-github/src/signature.rs                                     64       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                             285       1  99.65%   153
crates/acp-nats/src/nats/extensions.rs                                            3       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                  23       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                     102       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                         304       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                    88       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                       106       0  100.00%
crates/acp-nats/src/agent/new_session.rs                                         91       0  100.00%
crates/acp-nats/src/agent/logout.rs                                              49       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                       50       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                   71       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                           71       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                             123       4  96.75%   109-112
crates/acp-nats/src/agent/initialize.rs                                          83       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                             105       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                          92       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                             633       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                       101       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                       67       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                 65       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                    71       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                       299       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                        52       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                 220      10  95.45%   61-63, 77-79, 133-135, 158
crates/trogon-std/src/fs/system.rs                                               29      12  58.62%   17-19, 31-45
crates/acp-nats-ws/src/config.rs                                                 83       0  100.00%
crates/acp-nats-ws/src/connection.rs                                            166      35  78.92%   75-82, 87-98, 114, 116-117, 122, 133-135, 142, 146, 150, 153-161, 172, 176, 179, 182-186, 220
crates/acp-nats-ws/src/main.rs                                                  187      18  90.37%   87, 204-225, 303
crates/acp-nats-ws/src/upgrade.rs                                                57       2  96.49%   59, 90
crates/acp-nats-agent/src/connection.rs                                        1434       1  99.93%   686
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                 11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                     18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                    18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs               11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                     11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                   11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                   18       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                      58       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                        380       0  100.00%
TOTAL                                                                         19087     216  98.87%

Diff against main

Filename                                     Stmts    Miss  Cover
-----------------------------------------  -------  ------  -------
crates/trogon-source-linear/src/server.rs     +309     -39  +99.15%
TOTAL                                         +309     -39  +0.23%

Results for commit: 65d9885

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-source-linear/src/lib.rs (1)

20-23: Consider linking docs to the exported constant name.

Using a rustdoc link (e.g. [`crate::constants::NATS_HEADER_REJECT_REASON`]) instead of a raw header literal would reduce future drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-linear/src/lib.rs` around lines 20 - 23,
Replace the raw header literal in the crate-level doc comment with a rustdoc
link to the exported constant to avoid drift: change the
`{LINEAR_SUBJECT_PREFIX}.unroutable` doc or the `X-Linear-Reject-Reason` literal
to use a link like [`crate::constants::NATS_HEADER_REJECT_REASON`] (reference
the existing constant name NATS_HEADER_REJECT_REASON) so the documentation
points to the canonical constant; update the doc comment in lib.rs accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-source-linear/src/server.rs`:
- Around line 66-79: publish_unroutable currently swallows DLQ publish failures
by only logging outcome.log_on_error and the outer request handlers still return
400, which discards payloads when the DLQ is unavailable; change
publish_unroutable (and each validation-failure branch that calls it) to
propagate the publish outcome so callers can map a DLQ publish failure to a 500
instead of always returning 400: have publish_unroutable return a Result<(),
PublishError> (or the existing Outcome type) from publish_event rather than
void, surface that Result back to the request handler, and in the handlers where
publish_unroutable is invoked (the validation-failure branches that call
publish_event/publish_unroutable) convert an Err into an internal-server-error
response while still returning 400 for validation errors only when the DLQ
publish succeeded; keep logging via outcome.log_on_error but don't treat logging
as sufficient error handling.
- Around line 66-70: Replace the raw &str "reason" with a domain-specific
RejectReason enum/value object and update call sites: change the
publish_unroutable<P: JetStreamPublisher>(..., reason: &str, ...) signature to
accept a RejectReason (or &RejectReason) and implement conversion to the wire
string (Display/ToString or as_str) inside publish_unroutable; add a
RejectReason enum (with all allowed variants used at current call sites) and
factory/functions for construction so invalid reasons are unrepresentable, then
update every site that currently passes string literals (the call sites around
the ranges listed) to use the corresponding RejectReason variant or constructor
and ensure any serialization uses the enum’s string form when publishing.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-source-linear/src/lib.rs`:
- Around line 20-23: Replace the raw header literal in the crate-level doc
comment with a rustdoc link to the exported constant to avoid drift: change the
`{LINEAR_SUBJECT_PREFIX}.unroutable` doc or the `X-Linear-Reject-Reason` literal
to use a link like [`crate::constants::NATS_HEADER_REJECT_REASON`] (reference
the existing constant name NATS_HEADER_REJECT_REASON) so the documentation
points to the canonical constant; update the doc comment in lib.rs accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 113f1821-8b52-4bfd-89aa-95df60273b0b

📥 Commits

Reviewing files that changed from the base of the PR and between 430b2db and 0ac20b7.

📒 Files selected for processing (3)
  • rsworkspace/crates/trogon-source-linear/src/constants.rs
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
  • rsworkspace/crates/trogon-source-linear/src/server.rs

@yordis yordis force-pushed the yordis/linear-webhook-fields branch 5 times, most recently from 81305ca to f1a5abf Compare April 5, 2026 22:03
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/linear-webhook-fields branch from f1a5abf to 65d9885 Compare April 5, 2026 22:12
@yordis yordis merged commit 1c17914 into main Apr 5, 2026
7 checks passed
@yordis yordis deleted the yordis/linear-webhook-fields branch April 5, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant