Mugdha Sonare
12/10/2025, 2:13 PMMarvin
12/10/2025, 2:13 PMMarvin
12/10/2025, 2:21 PMimport os
import json
import requests
import azure.functions as func
PREFECT_WEBHOOK_URL = os.environ["PREFECT_WEBHOOK_URL"]
def main(req: func.HttpRequest) -> func.HttpResponse:
# Event Grid can use two schemas; check both patterns
# 1) CloudEvents schema: 'aeg-event-type: SubscriptionValidation' with a body containing validationCode
aeg_event_type = req.headers.get('aeg-event-type')
if aeg_event_type == 'SubscriptionValidation':
try:
data = req.get_json()
except ValueError:
data = {}
# Event Grid expects the validation code in a response header OR JSON, header is simplest:
validation_code = data.get('validationCode') or (data.get('data', {}) or {}).get('validationCode')
if validation_code:
# Respond with required header
return func.HttpResponse(status_code=200, headers={'aeg-validation-response': validation_code})
return func.HttpResponse("Missing validationCode", status_code=400)
# 2) Event Grid schema: array of events; validation event has eventType Microsoft.EventGrid.SubscriptionValidationEvent
try:
events = req.get_json()
except ValueError:
events = None
if isinstance(events, list) and events:
first = events[0]
if first.get('eventType') == 'Microsoft.EventGrid.SubscriptionValidationEvent':
validation_code = first.get('data', {}).get('validationCode')
if validation_code:
# Respond with JSON body per the Event Grid schema
return func.HttpResponse(
body=json.dumps({"validationResponse": validation_code}),
status_code=200,
mimetype="application/json",
)
# Not a validation event — forward to Prefect
# Forward body and basic headers; Prefect will return 204 on success
forward_headers = {'Content-Type': req.headers.get('Content-Type', 'application/json')}
try:
resp = <http://requests.post|requests.post>(PREFECT_WEBHOOK_URL, data=req.get_body(), headers=forward_headers, timeout=10)
# Return 200 to Event Grid when Prefect accepted the event (2xx)
if 200 <= resp.status_code < 300:
return func.HttpResponse(status_code=200)
return func.HttpResponse(f"Prefect returned {resp.status_code}", status_code=500)
except Exception as e:
return func.HttpResponse(f"Forwarding failed: {e}", status_code=500)Marvin
12/10/2025, 2:21 PM{{ body|from_cloud_event(headers) }}
Docs: Prefect Webhooks and Microsoft guidance: Event Grid endpoint validation
Other things to check (just in case)
- Did this subscription work previously? If yes, and it now fails only when editing, confirm that:
- The Prefect webhook URL hasn’t rotated
- You haven’t enabled “require authentication” for webhooks in your Prefect account — Event Grid cannot add a Prefect Bearer token. If authentication is enforced, you’ll need the small relay above to attach Authorization: Bearer <service-account-key> when forwarding to Prefect.
- Verify the Event Grid subscription schema (CloudEvents vs Event Grid schema). The function above handles both.
If you’d like, share:
- Whether this ever validated successfully before
- Your Event Grid schema choice (CloudEvents vs Event Grid schema)
- Whether webhook auth is enforced in your Prefect account/workspace
I can tailor the relay snippet (Function/Logic App) to your setup and the exact Prefect template you’re using.Mugdha Sonare
12/10/2025, 2:41 PMMarvin
12/10/2025, 2:47 PMMarvin
12/10/2025, 2:48 PMbody[0]), and it looks fine for normal deliveries.
What’s going on
- When you create or edit an Azure Event Grid webhook subscription, Azure sends a special validation request and expects a very specific response (either a JSON body with validationResponse for Event Grid schema, or an aeg-validation-response header for CloudEvents schema).
- Prefect Cloud webhooks return a generic 2xx/204 for normal events but do not emit the Microsoft-specific validation response. This typically causes validation to fail during create/edit.
- It’s possible your subscription validated successfully in the past due to differences in how the subscription was created, a prior intermediary/relay, or a change on the Azure side. However, with the current validation requirement, editing the subscription hits that handshake and fails.
Reference: Azure Event Grid endpoint validation and the open enhancement request: prefecthq/prefect#18122
Quick checks (to rule out other issues)
- Confirm the webhook URL hasn’t rotated and there’s no trailing whitespace.
- Manually POST a simple JSON payload to your Prefect webhook and confirm you get a 204:
curl -i -X POST "<https://api.prefect.cloud/hooks/<your_id>>" \
-H "Content-Type: application/json" \
-d '[{"eventType":"Microsoft.Storage.BlobCreated","subject":"/blobServices/default/containers/foo/blobs/bar.csv","data":{"url":"<https://account.blob.core.windows.net/foo/bar.csv>","contentType":"text/csv","contentLength":123}}]'
If you get 204, the webhook is healthy; the failure is isolated to Event Grid validation.
Recommended workaround (minimal relay)
Since Event Grid requires a handshake response, place a tiny Azure Function or Logic App in front of the Prefect webhook. It should:
- Detect validation requests and respond with the validation code in the format Azure expects
- Forward normal events to your Prefect webhook URL
- Optionally include any headers you want Prefect to see
Sample Azure Function (Python) that handles both schemas and forwards to Prefect:
```
import os
import json
import requests
import azure.functions as func
PREFECT_WEBHOOK_URL = os.environ["PREFECT_WEBHOOK_URL"]
def main(req: func.HttpRequest) -> func.HttpResponse:
# CloudEvents handshake: header aeg-event-type: SubscriptionValidation
aeg_event_type = req.headers.get('aeg-event-type')
if aeg_event_type == 'SubscriptionValidation':
try:
data = req.get_json()
except ValueError:
data = {}
validation_code = data.get('validationCode') or (data.get('data') or {}).get('validationCode')
if validation_code:
# Respond using header form expected by CloudEvents handshake
return func.HttpResponse(status_code=200, headers={'aeg-validation-response': validation_code})
return func.HttpResponse("Missing validationCode", status_code=400)
# Event Grid schema handshake: array with Microsoft.EventGrid.SubscriptionValidationEvent
try:
events = req.get_json()
except ValueError:
events = None
if isinstance(events, list) and events:
first = events[0]
if first.get('eventType') == 'Microsoft.EventGrid.SubscriptionValidationEvent':
validation_code = (first.get('data') or {}).get('validationCode')
if validation_code:
return func.HttpResponse(
body=json.dumps({"validationResponse": validation_code}),
status_code=200,
mimetype="application/json",
)
# Not validation — forward to Prefect
forward_headers = {'Content-Type': req.headers.get('Content-Type', 'application/json')}
try:
resp = requests.post(PREFECT_WEBHOOK_URL, data=req.get_body(), headers=forward_headers, timeout=10)Marvin
12/10/2025, 2:48 PMMugdha Sonare
12/10/2025, 2:52 PMMarvin
12/10/2025, 2:52 PMMarvin
12/10/2025, 2:55 PM<https://api.prefect.cloud/hooks/<your_webhook_id>> with no extra characters, no trailing dot, and no whitespace.
- Try switching the subscription to the CloudEvents schema (if you’re currently using the Event Grid schema). Validation behavior differs:
- CloudEvents validation expects the aeg-validation-response header.
- Event Grid schema expects a JSON body with {"validationResponse": "<code>"}.
- Verify the Prefect endpoint itself is healthy by posting a dummy event (should return 204):
curl -i -X POST "<https://api.prefect.cloud/hooks/<your_webhook_id>>" \
-H "Content-Type: application/json" \
-d '[{"eventType":"Microsoft.Storage.BlobCreated","subject":"/blobServices/default/containers/foo/blobs/bar.csv","data":{"url":"<https://account.blob.core.windows.net/foo/bar.csv>","contentType":"text/csv","contentLength":123}}]'
Why this is failing now
- Azure requires a special validation response when creating/editing webhook subscriptions. Prefect Cloud webhooks don’t currently emit the Microsoft-specific validation response, so the subscription fails at the handshake step even though normal event deliveries (post-validation) would work. This is a known limitation tracked here: Azure Event Grid Subscription Support.
- It could have worked previously if the subscription used the CloudEvents schema or there was a relay/proxy in place that handled validation.
Fastest path to green (recommended)
- Add a tiny Azure Function or Logic App in front of the Prefect webhook that:
- Responds to validation requests with the exact format Event Grid expects
- Forwards normal events to your Prefect webhook URL
- I can share a ready-to-deploy Logic App definition or the Azure Function sample from above. Which do you prefer?
Your existing Prefect template is fine for Event Grid deliveries:
{
"event": "azure.event_grid.blob_created",
"resource": {
"prefect.resource.id": "azure.storage.blob.{{ body[0]['subject'] }}",
"prefect.resource.name": "{{ body[0]['data']['url'].split('/')[-1] }}",
"eventType": "{{ body[0]['eventType'] }}"
},
"data": {
"eventType": "{{ body[0]['eventType'] }}",
"subject": "{{ body[0]['subject'] }}",
"blobUrl": "{{ body[0]['data']['url'] }}",
"contentType": "{{ body[0]['data']['contentType'] }}",
"contentLength": "{{ body[0]['data']['contentLength'] }}",
"aegSubscription": "{{ headers['aeg-subscription-name'] | default('') }}",
"aegEventType": "{{ headers['aeg-event-type'] | default('') }}"
}
}
If you want to try CloudEvents schema instead, you’d update the template to:
{{ body|from_cloud_event(headers) }}
But you’d still need the relay if Azure is enforcing the CloudEvents-style handshake where the header aeg-validation-response must be returned.
Helpful docs:
- Event Grid validation: aka.ms/esvalidation
- Prefect webhooks: Prefect Webhooks
Let me know:
- Which schema your subscription is set to (Event Grid vs CloudEvents)?
- Would you prefer a Logic App or Azure Function sample? I can provide a minimal template you can paste in and deploy.Mugdha Sonare
12/10/2025, 3:00 PMMugdha Sonare
12/10/2025, 3:01 PMMarvin
12/10/2025, 3:01 PMMarvin
12/10/2025, 3:02 PMeventDeliverySchema or inputSchema
- EventGridSchema = Event Grid schema
- CloudEventSchemaV1_0 = CloudEvents
- Azure CLI (show the property)
- For a resource-scoped subscription (e.g., Storage account):
az eventgrid event-subscription show \
--name <subscription-name> \
--source-resource-id <resource-id-of-the-source> \
--query "eventDeliverySchema"
- For a topic-scoped subscription (custom topic):
az eventgrid event-subscription show \
--name <subscription-name> \
--source-resource-id /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.EventGrid/topics/<topic> \
--query "eventDeliverySchema"
Will switching to CloudEvents make validation succeed?
- Probably not. The validation “handshake” is required for both schemas — the only difference is the response format Azure expects. If the endpoint does not implement Azure’s validation response, both schemas will fail at creation/edit time.
- This matches what you’re seeing: even brand-new subscriptions fail validation.
- Tracking issue: Azure Event Grid Subscription Support
If you still want to try CloudEvents anyway
- Create a new subscription with CloudEvents schema and point to your Prefect webhook.
- Update your template to parse CloudEvents:
{{ body|from_cloud_event(headers) }}
- Expectation: validation will likely still fail (same handshake requirement), but if Azure changes behavior and it passes, your events will parse correctly with the above template.
Direct integration vs minimal relay
- Direct: Given the current validation failure, switching schema won’t bypass the handshake requirement. If you need this working today, the reliable fix is a very thin relay (Logic App or Function) that:
- On validation → responds with the required validation code
- On normal events → forwards the payload to your Prefect webhook
- I can provide a ready-to-paste Logic App definition (no custom code), or the tiny Azure Function I shared earlier. Which do you prefer?
Quick sanity checks (just in case)
- Ensure the endpoint is exactly <https://api.prefect.cloud/hooks/<id>> (no trailing punctuation/whitespace).
- Confirm Prefect endpoint is healthy:
curl -i -X POST "<https://api.prefect.cloud/hooks/<your_id>>" \
-H "Content-Type: application/json" \
-d '[{"eventType":"Microsoft.Storage.BlobCreated","subject":"/blobServices/default/containers/foo/blobs/bar.csv","data":{"url":"<https://account.blob.core.windows.net/foo/bar.csv>","contentType":"text/csv","contentLength":123}}]'
You should see HTTP/1.1 204 No Content.
If you’d like the Logic App (no-code) relay, tell me your preference (Consumption vs Standard) and I’ll paste a minimal definition you can deploy in a few clicks.