diff --git a/samples/webhook-delivery-resilience/.devproxy/devproxyrc.json b/samples/webhook-delivery-resilience/.devproxy/devproxyrc.json new file mode 100644 index 0000000..d39a5be --- /dev/null +++ b/samples/webhook-delivery-resilience/.devproxy/devproxyrc.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v2.3.4/rc.schema.json", + "plugins": [ + { + "name": "RetryAfterPlugin", + "enabled": true, + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" + }, + { + "name": "GenericRandomErrorPlugin", + "enabled": true, + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", + "configSection": "transientWebhookErrors" + }, + { + "name": "MockResponsePlugin", + "enabled": true, + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", + "configSection": "mockResponsePlugin" + } + ], + "urlsToWatch": [ + "https://webhooks.contoso.com/*" + ], + "mockResponsePlugin": { + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v2.3.4/mockresponseplugin.schema.json", + "mocksFile": "mocks.json" + }, + "transientWebhookErrors": { + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v2.3.4/genericrandomerrorplugin.schema.json", + "errorsFile": "errors-transient.json", + "rate": 50 + }, + "logLevel": "information", + "newVersionNotification": "stable", + "showSkipMessages": true +} diff --git a/samples/webhook-delivery-resilience/.devproxy/errors-transient.json b/samples/webhook-delivery-resilience/.devproxy/errors-transient.json new file mode 100644 index 0000000..8099a6b --- /dev/null +++ b/samples/webhook-delivery-resilience/.devproxy/errors-transient.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v2.3.0/genericrandomerrorplugin.errorsfile.schema.json", + "errors": [ + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/transient" + }, + "responses": [ + { + "statusCode": 503, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Retry-After", + "value": "@dynamic" + } + ], + "body": { + "error": { + "code": "delivery_backpressure", + "message": "Receiver is temporarily overloaded. Retry later." + } + } + } + ] + } + ] +} diff --git a/samples/webhook-delivery-resilience/.devproxy/mocks.json b/samples/webhook-delivery-resilience/.devproxy/mocks.json new file mode 100644 index 0000000..f2a46e7 --- /dev/null +++ b/samples/webhook-delivery-resilience/.devproxy/mocks.json @@ -0,0 +1,129 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v2.3.0/mockresponseplugin.mocksfile.schema.json", + "mocks": [ + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/success", + "method": "POST" + }, + "response": { + "statusCode": 200, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "accepted": true, + "result": "processed", + "message": "Webhook accepted and processed." + } + } + }, + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/duplicate", + "method": "POST" + }, + "response": { + "statusCode": 409, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "error": { + "code": "duplicate_event", + "message": "Event ID already processed." + } + } + } + }, + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/out-of-order", + "method": "POST" + }, + "response": { + "statusCode": 409, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "error": { + "code": "out_of_order", + "message": "Event sequence is invalid for current resource state." + } + } + } + }, + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/invalid-signature", + "method": "POST" + }, + "response": { + "statusCode": 401, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "error": { + "code": "invalid_signature", + "message": "Signature validation failed." + } + } + } + }, + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/replayed", + "method": "POST" + }, + "response": { + "statusCode": 409, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "error": { + "code": "replay_detected", + "message": "Replay attack detected: timestamp outside accepted window." + } + } + } + }, + { + "request": { + "url": "https://webhooks.contoso.com/deliveries/transient", + "method": "POST" + }, + "response": { + "statusCode": 202, + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "body": { + "accepted": true, + "result": "queued", + "message": "Webhook queued for asynchronous processing." + } + } + } + ] +} diff --git a/samples/webhook-delivery-resilience/.vscode/extensions.json b/samples/webhook-delivery-resilience/.vscode/extensions.json new file mode 100644 index 0000000..cee8864 --- /dev/null +++ b/samples/webhook-delivery-resilience/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["garrytrinder.dev-proxy-toolkit"] +} diff --git a/samples/webhook-delivery-resilience/README.md b/samples/webhook-delivery-resilience/README.md new file mode 100644 index 0000000..97eaa0c --- /dev/null +++ b/samples/webhook-delivery-resilience/README.md @@ -0,0 +1,124 @@ +# Webhook delivery resilience testing + +Test how your app handles it when the webhook endpoint you're calling rejects duplicates, returns auth errors, or is temporarily unavailable — using Dev Proxy. + +![Dev Proxy simulating webhook delivery resilience scenarios](assets/snap-1.png) + +## Summary + +This sample simulates how a remote webhook endpoint can respond to your delivery attempts. Use it to validate how your sender handles duplicate-rejection errors, sequence conflicts, signature failures, and transient overloads. + +Dev Proxy intercepts calls to `https://webhooks.contoso.com/deliveries/*` and +returns the configured responses, so you can test locally before these failures +surface in production. + +## Typical use case + +Imagine your app is a platform that notifies downstream services, like a merchant +dashboard or a fulfilment system whenever key events occur. Your app sends events +like `payment.created` or `payment.succeeded` to an external webhook endpoint it +owns or manages. In real life, that endpoint may: + +- Reject a delivery because it already processed that event ID +- Reject a delivery because events arrived out of order +- Reject a delivery because the signature doesn't match +- Be temporarily overloaded and ask you to retry later + +This sample lets you test how your delivery code handles those responses locally +before they happen in production. + +## Compatibility + +![Dev Proxy v2.3.4](https://img.shields.io/badge/devproxy-v2.3.4-green.svg) + +## Contributors + +- [Lovy Jain](https://github.com/lovyjain) + +## Version history + +Version|Date|Comments +-------|----|-------- +1.0|April 30, 2026|Initial release + +## Minimal path to awesome + +- Get the sample: + - Download just this sample: + + ```bash + npx gitload-cli https://github.com/pnp/proxy-samples/tree/main/samples/webhook-delivery-resilience + ``` + + or + + - [Download as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/proxy-samples/tree/main/samples/webhook-delivery-resilience) and unzip it, or + - Clone this repository +- Start Dev Proxy: + + ```bash + devproxy + ``` + +- Exercise each scenario using curl through Dev Proxy: + + ```bash + # Happy path + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/success + + # Duplicate event ID + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/duplicate + + # Out-of-order event sequence + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/out-of-order + + # Invalid signature + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/invalid-signature + + # Replay detection + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/replayed + + # Transient failure / eventual success path + curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/transient + + # Run transient several times to see both outcomes (503 and 202) + for i in {1..10}; do curl -ikx http://127.0.0.1:8000 -X POST https://webhooks.contoso.com/deliveries/transient; done + ``` + +## Features + +This sample includes deterministic and transient webhook behaviors: + +- Duplicate event rejection (`409 duplicate_event`) +- Out-of-order event rejection (`409 out_of_order`) +- Signature validation failure (`401 invalid_signature`) +- Replay attack detection (`409 replay_detected`) +- Transient receiver overload with dynamic `Retry-After` (`503 delivery_backpressure`) +- Eventual success for transient delivery attempts (`202 queued`) + +Note: the transient route uses a 50% error rate, so repeated calls intentionally alternate between temporary failure and success. + +![Transient scenario showing mix of 503 and 202 responses](assets/snap-2.png) + +Using this sample you can: + +- Verify idempotency implementation for webhook event IDs +- Test sequence validation logic for related event types +- Validate signature verification and replay-window checks +- Exercise retry logic that respects `Retry-After` headers + +## Help + +We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues. + +You can try looking at [issues related to this sample](https://github.com/pnp/proxy-samples/issues?q=label%3A%22sample%3A%20webhook-delivery-resilience%22) to see if anybody else is having the same issues. + +If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/proxy-samples/issues/new). + +Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/proxy-samples/issues/new). + +## Disclaimer + +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +![](https://m365-visitor-stats.azurewebsites.net/SamplesGallery/pnp-devproxy-webhook-delivery-resilience) diff --git a/samples/webhook-delivery-resilience/assets/sample.json b/samples/webhook-delivery-resilience/assets/sample.json new file mode 100644 index 0000000..fa4959a --- /dev/null +++ b/samples/webhook-delivery-resilience/assets/sample.json @@ -0,0 +1,72 @@ +[ + { + "name": "pnp-devproxy-webhook-delivery-resilience", + "source": "pnp", + "title": "Webhook Delivery Resilience", + "shortDescription": "Test how your app handles it when the webhook endpoint you're calling rejects duplicates, returns auth errors, or is temporarily unavailable.", + "url": "https://github.com/pnp/proxy-samples/tree/main/samples/webhook-delivery-resilience", + "downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/proxy-samples/tree/main/samples/webhook-delivery-resilience", + "longDescription": [ + "Test how your app handles it when the webhook endpoint you're calling rejects duplicates, returns auth errors, or is temporarily unavailable." + ], + "creationDateTime": "2026-04-25", + "updateDateTime": "2026-05-01", + "products": [ + "Dev Proxy" + ], + "metadata": [ + { + "key": "SAMPLE ID", + "value": "webhook-delivery-resilience" + }, + { + "key": "PRESET", + "value": "No" + }, + { + "key": "MOCKS", + "value": "Yes" + }, + { + "key": "PLUGIN", + "value": "No" + }, + { + "key": "PROXY VERSION", + "value": "v2.3.4" + } + ], + "thumbnails": [ + { + "type": "image", + "order": 100, + "url": "https://github.com/pnp/proxy-samples/raw/main/samples/webhook-delivery-resilience/assets/snap-1.png", + "alt": "Dev Proxy simulating webhook delivery resilience scenarios" + } + ], + "authors": [ + { + "gitHubAccount": "lovyjain", + "pictureUrl": "https://github.com/lovyjain.png", + "name": "Lovy Jain" + } + ], + "references": [ + { + "name": "Get started with Dev Proxy", + "description": "Learn how to install and run Dev Proxy.", + "url": "https://learn.microsoft.com/microsoft-cloud/dev/dev-proxy/get-started" + }, + { + "name": "Mock responses", + "description": "Learn how to mock API responses with Dev Proxy.", + "url": "https://learn.microsoft.com/microsoft-cloud/dev/dev-proxy/how-to/mock-responses" + }, + { + "name": "Retry-after handling", + "description": "Learn how to test retry behavior with Retry-After.", + "url": "https://learn.microsoft.com/microsoft-cloud/dev/dev-proxy/technical-reference/retryafterplugin" + } + ] + } +] diff --git a/samples/webhook-delivery-resilience/assets/snap-1.png b/samples/webhook-delivery-resilience/assets/snap-1.png new file mode 100644 index 0000000..4f2df55 Binary files /dev/null and b/samples/webhook-delivery-resilience/assets/snap-1.png differ diff --git a/samples/webhook-delivery-resilience/assets/snap-2.png b/samples/webhook-delivery-resilience/assets/snap-2.png new file mode 100644 index 0000000..a8f62e6 Binary files /dev/null and b/samples/webhook-delivery-resilience/assets/snap-2.png differ