feat(webhook): add opt-in retries to IncomingWebhook and WebhookTrigger#2641
feat(webhook): add opt-in retries to IncomingWebhook and WebhookTrigger#2641zimeg wants to merge 18 commits into
Conversation
🦋 Changeset detectedLatest commit: 692179f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
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
left a comment
There was a problem hiding this comment.
🔏 More words for the amazing reviewers.
There was a problem hiding this comment.
✂️ note: Matches the @slack/web-api implementation:
| "p-retry": "^4", | ||
| "retry": "^0.13.1" |
There was a problem hiding this comment.
🔍 note: These packages align with the @slack/web-api implementations:
node-slack-sdk/packages/web-api/package.json
Lines 61 to 62 in 12b94a2
| // Strip transport-only options so they do not leak into the posted payload. | ||
| this.defaults.agent = undefined; | ||
| this.defaults.retryConfig = undefined; |
There was a problem hiding this comment.
📝 note: The default options passed to requests isn't ideal practice IMHO but we avoid regressions with this line.
WilliamBergamin
left a comment
There was a problem hiding this comment.
This looks great already 🚀
I think I might have mislead you around handling 429 responses, left a comment about this 😅
| 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(); |
| const retryable = status === 429 || status >= 500; | ||
| const wrapped = httpErrorWithOriginal(error); | ||
| throw retryable ? wrapped : new AbortError(wrapped); |
There was a problem hiding this comment.
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?
| 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); |
There was a problem hiding this comment.
@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.
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>
56cf76e to
3d9a0af
Compare
…etry Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
zimeg
left a comment
There was a problem hiding this comment.
🔭 Duplicated comments from changed code!
|
|
||
| export { addAppMetadata } from './instrument'; | ||
|
|
||
| export { default as retryPolicies, RetryOptions } from './retry-policies'; |
There was a problem hiding this comment.
🎁 note: Here we match the export pattern of the @slack/web-api package:
…webhook-retries # Conflicts: # packages/webhook/src/WebhookTrigger.test.ts
…webhook-retries # Conflicts: # packages/webhook/src/WebhookTrigger.ts
Summary
Adds opt-in retry support to
@slack/webhook, mirroring theretryConfigconvention already used by@slack/web-api'sWebClient. Stacked on top of #2615 (theWebhookTriggerclass), so this PR is scoped to just the retry change.IncomingWebhookandWebhookTriggeraccept a newretryConfig?: RetryOptionsconstructor option.retry-policies.tswith the same named policies exported by@slack/web-api—fiveRetriesInFiveMinutes,tenRetriesInAboutThirtyMinutes,rapidRetryPolicy— re-exported from the package entry point.send()is wrapped inp-retryusing the configured policy.Default: no retries (
{ retries: 0 }) whenretryConfigis unset — preserves today's behavior; retries are opt-in.Retry conditions (transient failures only):
5xx) → retry4xx), including rate limits (429) → abort immediatelyUnlike
@slack/web-api,429responses are not retried here. That client blocks its request queue to honor theRetry-Afterheader; 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 all4xxresponses are treated the same.retryConfigis a transport-only option and is stripped from the posted payload (likeagent).Consumed downstream by slackapi/slack-github-action#630, which passes its
0/5/10/RAPIDinput through to these policies.Requirements
🤖 Generated with Claude Code