<@ULVA73B9P> I am trying to edit the event grid su...
# ask-marvin
m
@Marvin I am trying to edit the event grid subscription which has a prefect web hook as the end point, but now when i try to edit that event grid subscription, it is not having a successful handshake with the webhook, i am getting this error on the event grid side -- Deployment has failed with the following error: {"code":"URL validation","message":"Webhook validation handshake failed for https://api.prefect.cloud/hooks/-VaLYQ0CXqeFWWjcpetstw. Http POST request failed with response code Unknown. For troubleshooting, visit https://aka.ms/esvalidation. Activity id:ef563813-ab93-47c7-80ac-017c19b5d431, timestamp: 12/9/2025 82232 PM (UTC)."}
m
thought for 508.6 seconds
Thanks for the details — that “URL validation / webhook validation handshake failed” message from Event Grid usually means the endpoint didn’t respond with the exact validation response Event Grid requires during subscription edits/creation. A few important points and next steps: What’s likely happening - Azure Event Grid requires a special validation handshake when you create or edit a webhook subscription. If the endpoint doesn’t echo back the validation code in the exact format Event Grid expects, the validation fails. - Prefect Cloud webhooks are designed to accept standard HTTP events and return 204 No Content on success. They don’t currently emit the Microsoft-specific validation handshake response that Event Grid expects during subscription validation, which explains the failure you’re seeing. - Reference: prefecthq/prefect#18122 (Azure Event Grid Subscription Support) Recommended pattern to integrate Event Grid with Prefect - Put a tiny Azure Function or Logic App in front of your Prefect webhook. It should: 1) Intercept the Event Grid validation request and respond with the validation code in the format Event Grid expects 2) For normal events, forward the request body (and useful headers) to your Prefect webhook URL 3) Return 200 to Event Grid (and optionally base success on Prefect’s response) Example: minimal Azure Function (Python) to handle validation + forward
Copy code
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:
    # 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)
- In Prefect, set your webhook template to accept the incoming payload as-is. If you use CloudEvents from Event Grid, a convenient template is:
Copy code
{{ 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.
m
@Marvin yes this event has been validated before and it was working fine, { "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('') }}" } } above is the custom template for the webhook, And there is not service account enforced for webhook authentication:
m
thought for 42.4 seconds
Thanks — that template confirms you’re using the Event Grid schema (array of events via
body[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:
Copy code
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)
# Return 200 to Event Grid if 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=502) except Exception as e: return func.HttpResponse(f"Forwarding failed: {e}", status_code=502) ``` Your Prefect template - Your current template is appropriate for normal Event Grid deliveries (array-of-events). It does not affect the validation phase; Azure’s handshake happens before Prefect would render the template. If you prefer a Logic App instead of a Function, I can share a minimal workflow definition that: - Responds to validation with the right header/body - Forwards non-validation events to your Prefect webhook URL A couple of questions to tailor this for you - Which schema is your subscription using today (Event Grid schema vs CloudEvents)? - Which Azure region hosts the Event Grid resource? - Are you creating a new subscription or editing an existing one? - Do you want a Logic App version of the relay instead of an Azure Function? Helpful links - Event Grid validation: aka.ms/esvalidation - Prefect webhooks overview: Prefect Webhooks
m
@Marvin it is an event subscription and i can also try creating a new webhook and new subscription, but even that is failing
m
thought for 147.2 seconds
Got it — thanks for confirming. Since both editing the existing subscription and creating a new one are failing, this points to the Event Grid validation handshake step (not your runtime template). A few focused checks you can try quickly: - Double-check the exact endpoint in the Event Grid subscription. That Azure error shows the URL twice with punctuation — make sure the endpoint is exactly: -
<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):
Copy code
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:
Copy code
{
  "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:
Copy code
{{ 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.
m
@Marvin
How can i find if it is event grid or cloud events? i dont have logic app or azure function, i just want it to work directly, do you recommend having the webhook with cloud event first and then mking it custom later after having a succesfull handshake? @Marvin
m
thought for 68.5 seconds
Great questions — here’s how to check which schema your Event Grid subscription uses and what to try next. How to tell if your subscription uses Event Grid schema vs CloudEvents - Azure Portal (existing subscription) - Open the subscription → Export template (or JSON view) → look for
eventDeliverySchema
or
inputSchema
-
EventGridSchema
= Event Grid schema -
CloudEventSchemaV1_0
= CloudEvents - Azure CLI (show the property) - For a resource-scoped subscription (e.g., Storage account):
Copy code
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):
Copy code
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:
Copy code
{{ 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:
Copy code
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.