Test GitHub webhooks on localhost
#

GitHub delivers webhooks to a public URL. Expose gives your local server one, labels each delivery with its event name, and lets you replay a delivery while you fix your handler.

1. Share your local server
#

expose share http://localhost:3000

Expose prints a public URL such as https://abc123.eu-1.sharedwithexpose.com. With Expose Pro, add --subdomain=my-app to keep the same URL between sessions.

2. Add the webhook in GitHub
#

For a repository:

  1. Go to Settings → Webhooks → Add webhook.
  2. Set Payload URL to your Expose URL plus your route, for example https://my-app.eu-1.sharedwithexpose.com/github/webhook.
  3. Set Content type to application/json.
  4. Enter a Secret. Use a long random string and store it in your app's environment.
  5. Choose the events you want and save.

For a GitHub App, set the same URL and secret as the Webhook URL and Webhook secret in the app's settings.

GitHub sends a ping event right away. It shows up in the Expose dashboard at http://127.0.0.1:4040.

3. Verify the signature
#

GitHub signs each delivery with your secret and sends the result in the X-Hub-Signature-256 header, as sha256= followed by an HMAC of the raw request body. Compute the same HMAC in your handler and compare the two with a constant-time comparison.

The event name is in the X-GitHub-Event header. Expose forwards both headers and the body unchanged.

4. Inspect and replay
#

Push a commit or open a pull request. In the Expose dashboard you see each delivery with its headers, payload and your response. The built-in GitHub request plugin labels deliveries with their event, such as push or pull_request.

Click Replay to send a delivery to your local server again after you change your code. GitHub can also redeliver from the webhook's Recent Deliveries tab.

Troubleshooting
#

  • GitHub shows a failed delivery. Check that Expose is still running and that the Payload URL matches the current Expose URL. Free URLs change on each connection.
  • The signature doesn't match. Verify against the raw body, not a re-encoded JSON string, and check that the content type is application/json.
  • You only want to look at payloads. Run expose catch-all and use its URL as the Payload URL. See Catch-All.