HomeContact

Local Webhook Testing Using ngrok to Connect GitHub Actions with Your .NET API

By Shady Nagy
Published in Github
April 25, 2025
4 min read
Local Webhook Testing Using ngrok to Connect GitHub Actions with Your .NET API

Table Of Contents

01
Introduction
02
The Local Development Dilemma
03
Prerequisites
04
Setting Up Your .NET API
05
Exposing Your API with ngrok
06
Configuring GitHub Actions to Use Your ngrok URL
07
Creating a More Robust Setup
08
Enhanced .NET API with Logging and Security
09
Testing the Complete Flow
10
Debugging Tips
11
Security Considerations
12
Advanced: Using a Custom GitHub Action
13
Real-World Use Cases
14
Conclusion
15
Resources
16
Feedback and Questions

Introduction

As developers, we frequently face a common challenge: how do we test webhook-driven workflows that depend on services running locally? More specifically, how do we validate GitHub Actions that need to communicate with APIs hosted on our development machines?

Enter ngrok — a powerful tunneling tool that creates secure tunnels to expose your local services to the internet. In this guide, I’ll walk you through using ngrok to create a bridge between GitHub Actions and your local .NET API endpoints, enabling seamless testing without deploying to a staging environment.

The Local Development Dilemma

Before diving into the solution, let’s understand the problem:

  1. You’re developing a .NET API locally (typically accessible only via localhost)
  2. You’ve created GitHub Actions workflows that need to interact with your API
  3. GitHub’s runners can’t directly access your local development environment
  4. You want to test the complete workflow without deploying your API to a publicly accessible server

This is where ngrok shines, creating a secure tunnel that makes your local services temporarily accessible from anywhere.

Prerequisites

Before we begin, make sure you have:

  • .NET SDK installed
  • A GitHub repository with GitHub Actions configured
  • ngrok account and CLI tool installed
  • Basic familiarity with webhooks and API development

Setting Up Your .NET API

Let’s start with a simple .NET API that we’ll expose via ngrok. For this example, I’ll create a basic webhook receiver that logs incoming GitHub event data.

using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/api/github-webhook", async (HttpContext context) =>
{
// Read the request body
using var reader = new StreamReader(context.Request.Body);
var requestBody = await reader.ReadToEndAsync();
// Parse the JSON payload
var payload = JsonSerializer.Deserialize<JsonDocument>(requestBody);
// Log the event (in a real app, you'd process this based on your requirements)
Console.WriteLine($"Received GitHub webhook: {payload?.RootElement.GetProperty("action")}");
Console.WriteLine($"Event payload: {requestBody}");
// Return a success response
return Results.Ok(new { message = "Webhook received successfully" });
});
app.Run("http://localhost:5000");

Save this as Program.cs in a new .NET project. This creates a simple API with a single endpoint that accepts GitHub webhook payloads.

To run this API locally:

dotnet run

Your API is now running at http://localhost:5000, but it’s not accessible from the internet yet.

Exposing Your API with ngrok

Now, let’s use ngrok to create a public URL for your local API:

  1. Start ngrok to create a tunnel to your local service
ngrok http 5000
  1. ngrok provides a public URL that forwards to your local service

You’ll see output similar to this:

Session Status online
Account your-account (Plan: Free)
Version 3.3.1
Region United States (us)
Latency 51ms
Web Interface http://127.0.0.1:4040
Forwarding https://a1b2c3d4e5f6.ngrok.io -> http://localhost:5000

The important part is the Forwarding URL (https://a1b2c3d4e5f6.ngrok.io in this example). This is your temporary public URL that routes to your local API.

Note: Each time you restart ngrok, you’ll get a different URL. For persistent URLs, consider upgrading to a paid ngrok plan.

Configuring GitHub Actions to Use Your ngrok URL

Now we’ll set up a GitHub Action workflow that interacts with your local API via the ngrok tunnel:

name: Test Local API Integration
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
test-webhook:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Send test webhook to local API
run: |
curl -X POST \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-d '{"action": "test", "sender": {"login": "github-actions"}}' \
https://a1b2c3d4e5f6.ngrok.io/api/github-webhook

Save this as .github/workflows/test-local-api.yml in your repository.

Important: Replace https://a1b2c3d4e5f6.ngrok.io with your actual ngrok URL from the previous step.

Creating a More Robust Setup

The basic example works, but in a real development workflow, you need something more robust. Let’s improve our setup:

1. Making the ngrok URL configurable

Instead of hardcoding the ngrok URL, let’s use GitHub repository secrets:

name: Test Local API Integration
on:
push:
branches: [ main ]
workflow_dispatch:
inputs:
ngrok_url:
description: 'Current ngrok URL (without trailing slash)'
required: true
jobs:
test-webhook:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Send test webhook to local API
run: |
# Use the input URL if provided, otherwise use the secret
WEBHOOK_URL="${{ github.event.inputs.ngrok_url || secrets.NGROK_URL }}"
curl -X POST \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-d '{"action": "test", "sender": {"login": "github-actions"}}' \
$WEBHOOK_URL/api/github-webhook

This allows you to:

  • Store a default ngrok URL as a repository secret (NGROK_URL)
  • Override it when manually triggering the workflow

2. Creating a bash script for ngrok management

To make the ngrok setup easier to use, create a start-ngrok.sh script:

#!/bin/bash
# Kill any running ngrok process
pkill -f ngrok || true
# Start ngrok in the background
ngrok http 5000 > /dev/null &
# Wait for ngrok to initialize
sleep 3
# Get the public URL
NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
echo "====================================================="
echo "ngrok tunnel is running!"
echo "Public URL: $NGROK_URL"
echo "====================================================="
echo ""
echo "To update your GitHub repository with this URL, run:"
echo "gh secret set NGROK_URL --body=\"$NGROK_URL\""
echo ""
echo "Or use this URL when manually triggering the workflow"
echo "====================================================="
# Keep the script running to maintain the tunnel
wait

Make it executable:

chmod +x start-ngrok.sh

Now you can start ngrok with a simple command and easily update your GitHub secret with the new URL.

Enhanced .NET API with Logging and Security

Let’s improve our .NET API to be more production-ready:

using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddLogging();
var app = builder.Build();
// GitHub webhook secret
string webhookSecret = builder.Configuration["GitHub:WebhookSecret"] ?? "your-webhook-secret";
app.MapPost("/api/github-webhook", async (HttpContext context, ILogger<Program> logger) =>
{
try
{
// Verify signature if X-Hub-Signature-256 is present
if (context.Request.Headers.TryGetValue("X-Hub-Signature-256", out var signatureWithPrefix))
{
// Buffer the request body so we can read it multiple times
context.Request.EnableBuffering();
using var reader = new StreamReader(
context.Request.Body,
encoding: Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
leaveOpen: true);
var requestBody = await reader.ReadToEndAsync();
// Rewind the stream for later use
context.Request.Body.Position = 0;
// Verify signature
if (!VerifyGitHubWebhook(requestBody, webhookSecret, signatureWithPrefix))
{
logger.LogWarning("Invalid webhook signature");
return Results.Unauthorized();
}
}
// Read the event type
var eventType = context.Request.Headers["X-GitHub-Event"].ToString();
// Read the request body (again)
using var bodyReader = new StreamReader(context.Request.Body);
var body = await bodyReader.ReadToEndAsync();
// Parse the JSON payload
var payload = JsonSerializer.Deserialize<JsonDocument>(body);
// Log the event
logger.LogInformation(
"Received GitHub webhook: {EventType}, Action: {Action}",
eventType,
payload?.RootElement.TryGetProperty("action", out var action) ? action.ToString() : "none"
);
// Here you would normally process the webhook based on the event type
// For example, handling push events, pull requests, etc.
return Results.Ok(new { message = $"Processed {eventType} webhook successfully" });
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing webhook");
return Results.Problem("Error processing webhook");
}
});
app.Run("http://localhost:5000");
// GitHub webhook signature verification
bool VerifyGitHubWebhook(string payload, string secret, string signatureWithPrefix)
{
var secretBytes = Encoding.ASCII.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA256(secretBytes);
var hash = hmac.ComputeHash(payloadBytes);
var expectedSignature = "sha256=" + string.Concat(hash.Select(b => b.ToString("x2")));
return SignatureEquals(expectedSignature, signatureWithPrefix);
}
// Constant-time comparison to prevent timing attacks
bool SignatureEquals(string a, string b)
{
if (a.Length != b.Length)
{
return false;
}
int result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return result == 0;
}

This enhanced API adds:

  • Proper signature verification for GitHub webhooks
  • Better error handling
  • Structured logging
  • Constant-time signature comparison for security

Testing the Complete Flow

Now let’s test the entire workflow:

  1. Start your .NET API

    dotnet run
  2. Start ngrok using the script

    ./start-ngrok.sh

    Copy the generated URL.

  3. Update your GitHub Action Either:

    • Update the repository secret: gh secret set NGROK_URL --body="https://your-ngrok-url"
    • Or manually trigger the workflow and provide the URL
  4. Run the GitHub Action

    • Push a commit to trigger the workflow, or
    • Manually trigger it from the GitHub Actions tab
  5. Check your .NET API logs You should see the incoming webhook from GitHub Actions.

Debugging Tips

When working with ngrok and GitHub Actions, you might encounter some issues. Here are some tips:

ngrok Debugging

  1. Access the ngrok inspection interface Open http://localhost:4040 in your browser to see all requests passing through your ngrok tunnel.

  2. Enable detailed logging in ngrok

    ngrok http 5000 --log=stdout
  3. Verify connectivity

    curl -v https://your-ngrok-url.ngrok.io/api/github-webhook

.NET API Debugging

  1. Enable developer exception page Add app.UseDeveloperExceptionPage(); to your .NET API to see detailed error information.

  2. Check request headers Log all incoming headers to understand what GitHub is sending:

    foreach (var header in context.Request.Headers)
    {
    Console.WriteLine($"{header.Key}: {header.Value}");
    }
  3. Test locally Send test webhooks directly to your API to verify it works correctly:

    curl -X POST \
    -H "Content-Type: application/json" \
    -H "X-GitHub-Event: ping" \
    -d '{"action":"test"}' \
    http://localhost:5000/api/github-webhook

Security Considerations

When exposing your local services with ngrok, consider these security practices:

  1. Use webhook secrets Always verify webhook signatures using a shared secret.

  2. Limit exposed endpoints Only expose the specific endpoints needed for testing.

  3. Use ngrok’s IP restrictions With paid plans, you can restrict which IPs can access your ngrok tunnel.

  4. Don’t use production data Use test data in your development environment.

  5. Temporary credentials If your API requires authentication, use temporary credentials for testing.

Advanced: Using a Custom GitHub Action

For more complex scenarios, you might want to create a custom GitHub Action that handles communication with your local API. Here’s a simple example:

name: Custom Webhook Action
on:
push:
branches: [ main ]
workflow_dispatch:
inputs:
ngrok_url:
description: 'Current ngrok URL'
required: true
jobs:
send-webhook:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0.x'
- name: Build webhook sender
run: |
dotnet new console -o WebhookSender
cd WebhookSender
# Add dependencies
dotnet add package System.CommandLine
dotnet add package Newtonsoft.Json
# Create the webhook sender program
cat << EOF > Program.cs
using System.CommandLine;
using System.Text;
using Newtonsoft.Json;
using System.Net.Http.Headers;
// Define command-line options
var urlOption = new Option<string>("--url", "The webhook URL to send data to");
var eventOption = new Option<string>("--event", "The GitHub event type");
var dataOption = new Option<string>("--data", "JSON payload to send");
var rootCommand = new RootCommand("GitHub webhook sender");
rootCommand.AddOption(urlOption);
rootCommand.AddOption(eventOption);
rootCommand.AddOption(dataOption);
rootCommand.SetHandler(async (url, eventType, data) =>
{
using var httpClient = new HttpClient();
// Create request message
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Add("X-GitHub-Event", eventType);
request.Headers.Add("User-Agent", "GitHub-Actions-Webhook-Sender");
// Add the payload
var content = new StringContent(data, Encoding.UTF8, "application/json");
request.Content = content;
// Send the request
Console.WriteLine($"Sending {eventType} webhook to {url}");
var response = await httpClient.SendAsync(request);
// Process the response
var responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine(responseBody);
if (!response.IsSuccessStatusCode)
{
Environment.Exit(1);
}
}, urlOption, eventOption, dataOption);
return rootCommand.Invoke(args);
EOF
# Build the webhook sender
dotnet build
- name: Send webhook to local API
run: |
cd WebhookSender
TARGET_URL="${{ github.event.inputs.ngrok_url || secrets.NGROK_URL }}/api/github-webhook"
dotnet run -- \
--url "$TARGET_URL" \
--event "workflow_run" \
--data '{
"action": "completed",
"workflow_run": {
"id": ${{ github.run_id }},
"name": "${{ github.workflow }}",
"conclusion": "success"
},
"repository": {
"full_name": "${{ github.repository }}"
}
}'

This action creates a small .NET application that sends a webhook to your local API with customizable event types and payloads.

Real-World Use Cases

Here are some practical scenarios where this ngrok-GitHub Actions integration is valuable:

  1. Testing webhooks during development Validate webhook handlers without deploying.

  2. Debugging complex workflows Diagnose issues in GitHub Actions that interact with your API.

  3. Testing integration between services Verify how your service responds to GitHub events.

  4. Local preview of deployment workflows Test deployment processes without affecting production.

  5. Feature branch testing Test new API features with existing CI/CD pipelines.

Conclusion

Using ngrok to connect GitHub Actions with your local .NET API creates a powerful development workflow that saves time and reduces the need for deployment just for testing. By following the approach outlined in this guide, you can:

  • Test webhook integrations locally
  • Debug GitHub Actions workflows efficiently
  • Validate end-to-end processes during development
  • Improve your development cycle time

Remember to consider security implications when exposing local services, and always use webhook signature verification in production environments.

Resources

Feedback and Questions

Your insights drive us! For any questions, feedback, or thoughts, feel free to connect:

  1. Email: shady@shadynagy.com
  2. Twitter: @ShadyNagy_
  3. LinkedIn: Shady Nagy
  4. GitHub: ShadyNagy

If you found this guide beneficial, don’t hesitate to share it with your network. Until the next guide, happy coding! 🚀


Tags

#ngrok#GitHubActions#WebhookTesting#LocalDevelopment#dotNET#CICD#APITesting#DevTools#SoftwareTesting#WebDevelopment#DevWorkflow#csharp#ASPNETCore#TechTips#DeveloperTools

Share


Previous Article
InternalsVisibleTo Exposing Internal Members to Test Projects in .NET
Shady Nagy

Shady Nagy

Software Innovation Architect

Topics

AI
Angular
dotnet
GatsbyJS
Github
Linux
MS SQL
Oracle

Related Posts

Solving Windows Path Length Limitations in Git
Solving Windows Path Length Limitations in Git
May 10, 2025
4 min

Quick Links

Contact Us

Social Media