-
Notifications
You must be signed in to change notification settings - Fork 18
feat: support standard webhook delivery mode #519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@claude can you review please? |
Claude finished @alexluong's task —— View job Code Review
SummaryThis is a solid implementation that adds Standard Webhooks specification support to Outpost. The code is well-structured, thoroughly tested, and includes extensive documentation. The implementation correctly reuses existing signature infrastructure while properly enforcing the Standard Webhooks format requirements. Strengths✅ Excellent test coverage - Comprehensive validation, publishing, and integration tests including verification with the official Standard Webhooks SDK Issues Found🔴 Critical Issues1. Response body consumed twice in error path (destwebhookstandard.go:536-548) In the // Line 536
bodyBytes, _ := io.ReadAll(resp.Body) // First read
delivery := &destregistry.Delivery{
Status: "failed",
Code: fmt.Sprintf("%d", resp.StatusCode),
}
parseResponse(delivery, resp) // Second read attempt - body is already consumed! Fix: Either pass if resp.StatusCode >= 400 {
bodyBytes, _ := io.ReadAll(resp.Body)
delivery := &destregistry.Delivery{
Status: "failed",
Code: fmt.Sprintf("%d", resp.StatusCode),
Response: map[string]interface{}{
"status": resp.StatusCode,
"body": string(bodyBytes),
},
}
return delivery, destregistry.NewErrDestinationPublishAttempt(...)
} 🟡 Medium Priority Issues2. Inconsistent metadata.json description (metadata.json:16) The description states: "whsec_ secrets and msg_ prefixed message IDs" but the PR notes explicitly mention that message IDs do NOT follow the "msg_" prefix requirement:
Fix: Update the description to be accurate: "description": "Send events as Standard Webhooks (https://www.standardwebhooks.com/) compliant webhooks with whsec_ secrets. Note: Message IDs use the existing event ID format." 3. Header prefix validation missing (destwebhookstandard.go:107-113) The func WithHeaderPrefix(prefix string) Option {
return func(d *StandardWebhookDestination) {
if prefix != "" {
// Validate prefix format
if !strings.HasSuffix(prefix, "-") && !strings.HasSuffix(prefix, "_") {
// Consider logging a warning or returning an error
}
d.headerPrefix = prefix
}
}
} 4. Default header prefix inconsistency (destinations.go:54, default.go:58) The configuration documentation states the default is "webhook-" in standard mode, but the code sets this in two places which could drift:
Recommendation: Set the default consistently in one place (preferably in config conversion). 5. Missing error handling for credential parsing (destwebhookstandard.go:277) The time.Parse error returns a validation error, but doesn't preserve the underlying error message which would help debugging: if err != nil {
return nil, nil, destregistry.NewErrDestinationValidation([]destregistry.ValidationErrorDetail{{
Field: "credentials.previous_secret_invalid_at",
Type: "pattern",
// Consider adding: Message: err.Error(),
}})
} 🟢 Minor Issues / Suggestions6. Redundant nil check (destwebhookstandard.go:250-254) The triple condition check could be simplified with a helper function: func hasCredentials(creds map[string]string) bool {
return creds["secret"] != "" || creds["previous_secret"] != "" || creds["previous_secret_invalid_at"] != ""
} 7. Magic number for previous secret timestamp (destwebhookstandard.go:194) The // Set to 1 hour before current secret for ordering
// The actual created time is unknown, so we use a reasonable default
secrets = append(secrets, destwebhook.WebhookSecret{
Key: parsedPrevSecret,
CreatedAt: now.Add(-1 * time.Hour),
InvalidAt: creds.PreviousSecretInvalidAt,
}) 8. Duplicate response parsing logic (destwebhookstandard.go:626-648) The func parseResponse(delivery *destregistry.Delivery, resp *http.Response) {
bodyBytes, _ := io.ReadAll(resp.Body)
if strings.Contains(resp.Header.Get("Content-Type"), "application/json") {
var response map[string]interface{}
if err := json.Unmarshal(bodyBytes, &response); err == nil {
delivery.Response = map[string]interface{}{
"status": resp.StatusCode,
"body": response,
}
return
}
}
// Fallback to string body
delivery.Response = map[string]interface{}{
"status": resp.StatusCode,
"body": string(bodyBytes),
}
} 9. Test assertion helper could be more specific (assert_test.go:100) The comment "Note: webhook-id format depends on event.ID format (user-provided)" suggests the test should verify this more explicitly rather than just checking non-empty. Architecture & DesignRefactoring Opportunity (acknowledged in PR description) The PR author correctly identifies code duplication between
Why not refactor now? The author's reasoning is sound - avoiding breaking changes and keeping the initial PR focused is the right call. Configuration & DocumentationWell done:
Suggestion: Add an example in the instructions.md showing how to verify webhooks using curl or a simple code snippet. Test CoverageExcellent coverage:
Minor gap: No explicit test for the configurable header prefix feature. Consider adding: t.Run("should use custom header prefix", func(t *testing.T) {
// Test with prefix "x-custom-"
}) SecurityStrong security posture:
One consideration: Secrets are not obfuscated in PerformanceNo significant concerns:
Minor optimization: Consider pooling the JSON marshaling buffers if webhook throughput becomes a bottleneck. VerdictThis is high-quality code that's ready for merge after addressing the critical response body issue. The implementation follows Go best practices, has excellent test coverage, and properly adheres to the Standard Webhooks specification. Recommended ActionsMust fix:
Should fix: Nice to have: Great work! 🎉 Job Run | Branch: |
This PR allows operators to send webhooks following the Standard Webhooks specification (https://www.standardwebhooks.com/).
Configuration
DESTINATIONS_WEBHOOK_MODE
: Set to"standard"
to enable Standard Webhooks mode (defaults to"default"
)DESTINATIONS_WEBHOOK_HEADER_PREFIX
: Customize header prefix (defaults to"webhook-"
in standard mode,"x-outpost-"
in default mode)Spec
https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md
Example Headers
webhook-id: evt_123abc
webhook-timestamp: 1234567890
webhook-signature: v1,ZqFuVEt+GFBM5Bysq7W0liR22gpbVNv50qLOwYGdVqM=
webhook-topic: user.created
Implementation
The implementation imports some logic from
destwebhook
(signature generation) and duplicates other logic (validation, rotation, preprocessing). If we want to extend or maintain both providers moving forward, we should refactorshared logic into common helpers to avoid drift.
The test suite imports the official Standard Webhooks Go SDK to verify webhook deliveries conform to the spec.
Notes
whsec_
prefix with base64-encoded secrets per spec"msg_"
prefix. This PR uses the existing event ID format directly. Support for configurable event ID templates/generators will be addressed in a future PR.