Skip to content

feat(webhook): add opt-in retries to IncomingWebhook and WebhookTrigger#2641

Open
zimeg wants to merge 18 commits into
eden/webhook-triggerfrom
eden/webhook-retries
Open

feat(webhook): add opt-in retries to IncomingWebhook and WebhookTrigger#2641
zimeg wants to merge 18 commits into
eden/webhook-triggerfrom
eden/webhook-retries

Conversation

@zimeg

@zimeg zimeg commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Adds opt-in retry support to @slack/webhook, mirroring the retryConfig convention already used by @slack/web-api's WebClient. Stacked on top of #2615 (the WebhookTrigger class), so this PR is scoped to just the retry change.

  • Both IncomingWebhook and WebhookTrigger accept a new retryConfig?: RetryOptions constructor option.
  • Adds retry-policies.ts with the same named policies exported by @slack/web-apifiveRetriesInFiveMinutes, tenRetriesInAboutThirtyMinutes, rapidRetryPolicy — re-exported from the package entry point.
  • Each send() is wrapped in p-retry using the configured policy.

Default: no retries ({ retries: 0 }) when retryConfig is unset — preserves today's behavior; retries are opt-in.

Retry conditions (transient failures only):

  • Server errors (5xx) → retry
  • Network errors with no response → retry
  • All client errors (4xx), including rate limits (429) → abort immediately

Unlike @slack/web-api, 429 responses are not retried here. That client blocks its request queue to honor the Retry-After header; the webhook clients have no such queue, so a blind retry would ignore the server's backoff. Rate-limit handling is left to the caller, and all 4xx responses are treated the same.

retryConfig is a transport-only option and is stripped from the posted payload (like agent).

Consumed downstream by slackapi/slack-github-action#630, which passes its 0/5/10/RAPID input through to these policies.

Requirements

🤖 Generated with Claude Code

@zimeg zimeg added semver:minor enhancement M-T: A feature request for new functionality pkg:webhook applies to `@slack/webhook` labels Jul 2, 2026
@zimeg zimeg self-assigned this Jul 2, 2026
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 692179f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@slack/webhook Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.81633% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.06%. Comparing base (e6604b6) to head (692179f).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                   Coverage Diff                    @@
##           eden/webhook-trigger    #2641      +/-   ##
========================================================
+ Coverage                 88.97%   89.06%   +0.09%     
========================================================
  Files                        64       65       +1     
  Lines                     10363    10436      +73     
  Branches                    467      471       +4     
========================================================
+ Hits                       9220     9295      +75     
+ Misses                     1122     1120       -2     
  Partials                     21       21              
Flag Coverage Δ
cli-hooks 89.06% <90.81%> (+0.09%) ⬆️
cli-test 89.06% <90.81%> (+0.09%) ⬆️
logger 89.06% <90.81%> (+0.09%) ⬆️
oauth 89.06% <90.81%> (+0.09%) ⬆️
socket-mode 89.06% <90.81%> (+0.09%) ⬆️
web-api 89.06% <90.81%> (+0.09%) ⬆️
webhook 89.06% <90.81%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

zimeg and others added 8 commits July 2, 2026 02:25
Default the payload to an empty object so send() can be called with no
arguments, POSTing an empty body. send({}) continues to work unchanged.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
…webhook-retries

# Conflicts:
#	packages/webhook/src/WebhookTrigger.ts
The `= {}` default now lives in the WebhookTrigger PR; keep the @param
description unchanged here.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Use a falsy check so an empty-string URL is rejected up front rather
than failing later at request time. Adds a guard test.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
@zimeg zimeg marked this pull request as ready for review July 3, 2026 19:55
@zimeg zimeg requested a review from a team as a code owner July 3, 2026 19:55

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔏 More words for the amazing reviewers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment thread packages/webhook/package.json Outdated
Comment on lines +48 to +49
"p-retry": "^4",
"retry": "^0.13.1"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔍 note: These packages align with the @slack/web-api implementations:

"p-retry": "^4",
"retry": "^0.13.1"

Comment on lines +61 to +63
// Strip transport-only options so they do not leak into the posted payload.
this.defaults.agent = undefined;
this.defaults.retryConfig = undefined;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

📝 note: The default options passed to requests isn't ideal practice IMHO but we avoid regressions with this line.

@zimeg zimeg added this to the webhook@next milestone Jul 3, 2026

@WilliamBergamin WilliamBergamin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks great already 🚀

I think I might have mislead you around handling 429 responses, left a comment about this 😅

Comment on lines +155 to +168
it('does not retry a 4xx even when a retry policy is set', async () => {
const scope = nock('https://hooks.slack.com')
.post(/triggers/)
.reply(400);
const trigger = new WebhookTrigger(url, { retryConfig: rapidRetryPolicy });
try {
await trigger.send({ key: 'value' });
assert.fail('expected rejection');
} catch (error) {
assert.strictEqual((error as CodedError).code, ErrorCode.HTTPError);
}
// Only one interceptor is registered; a retry would leave it unmatched
// and scope.done() would throw.
scope.done();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Praise 💯

Comment thread packages/webhook/src/WebhookTrigger.ts Outdated
Comment thread packages/webhook/src/IncomingWebhook.ts Outdated
Comment on lines +89 to +91
const retryable = status === 429 || status >= 500;
const wrapped = httpErrorWithOriginal(error);
throw retryable ? wrapped : new AbortError(wrapped);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My mistake here, I may have mislead you on this 😅

429 status codes usually come with a retry header that informs the client on when it is appropriate to send requests. The web-api handles this by blocking the request queue of the client

The webhook package does not have the concept of request queues, and no one asked for it 🤔 Currently 429 responses are handled by the user, since we aren't taking into account the retry header in this retry implementation what do you think about treating all 4** responses the same way?

Suggested change
const retryable = status === 429 || status >= 500;
const wrapped = httpErrorWithOriginal(error);
throw retryable ? wrapped : new AbortError(wrapped);
const wrapped = httpErrorWithOriginal(error);
throw status >= 500 ? wrapped : new AbortError(wrapped);

@zimeg zimeg Jul 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@WilliamBergamin Ah I'm learning more about HTTP! Thanks for sharing this!

We're landing a change in 3d9a0af to save retries for server errors at this time. I agree it'd be alright to wait for feedback before extending this more.

zimeg added a commit that referenced this pull request Jul 6, 2026
The webhook clients have no request queue to honor a Retry-After header,
so a 429 cannot respect the server's backoff on retry. Treat all 4xx
responses uniformly and leave rate-limit handling to the caller; only
5xx responses are retried. Addresses review feedback on #2641.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
The webhook clients have no request queue to honor a Retry-After header,
so a 429 cannot respect the server's backoff on retry. Treat all 4xx
responses uniformly and leave rate-limit handling to the caller; only
5xx responses are retried. Addresses review feedback on #2641.

Co-Authored-By: William Bergamin <25348381+WilliamBergamin@users.noreply.github.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
@zimeg zimeg force-pushed the eden/webhook-retries branch from 56cf76e to 3d9a0af Compare July 6, 2026 22:44
zimeg and others added 2 commits July 6, 2026 23:47

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔭 Duplicated comments from changed code!

Comment thread packages/webhook/src/index.ts Outdated

export { addAppMetadata } from './instrument';

export { default as retryPolicies, RetryOptions } from './retry-policies';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🎁 note: Here we match the export pattern of the @slack/web-api package:

export { default as retryPolicies, RetryOptions } from './retry-policies';

zimeg added 4 commits July 7, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality pkg:webhook applies to `@slack/webhook` semver:minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants