Skip to content

Commit 8fe5a1b

Browse files
committed
Improves connection resilience with auto-reconnect
Enhances the CLI's robustness by adding an automatic reconnection feature when the SSE connection is lost. This includes: - Exponential backoff strategy for reconnection attempts. - Limits the number of reconnection attempts to avoid indefinite loops. - Introduces proper escaping of response body content to prevent markup errors in the console. - Manual disconnect disables auto-reconnect - Updates version to 1.0.17
1 parent 8a5607b commit 8fe5a1b

3 files changed

Lines changed: 88 additions & 5 deletions

File tree

HookReplay.Cli.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<TrimMode>link</TrimMode>
2626

2727
<!-- Package metadata for NuGet -->
28-
<Version>1.0.8</Version>
28+
<Version>1.0.17</Version>
2929
<Authors>HookReplay</Authors>
3030
<Company>HookReplay</Company>
3131
<Description>Debug webhooks locally with HookReplay CLI. Capture, inspect, and replay webhooks to your local development server.</Description>

Program.cs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ internal class Program
3939
private static CancellationTokenSource? _connectionCts;
4040
private static Task? _sseListenerTask;
4141
private static string? _latestVersion;
42+
private static string? _currentSseUrl;
43+
private static string? _currentApiKey;
44+
private static bool _autoReconnectEnabled = true;
45+
private static int _reconnectAttempts = 0;
46+
private const int MaxReconnectAttempts = 10;
47+
private static readonly int[] ReconnectDelaysMs = [1000, 2000, 5000, 10000, 30000];
4248

4349
private static async Task<int> Main(string[] args)
4450
{
@@ -584,6 +590,12 @@ await AnsiConsole.Status()
584590
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
585591

586592
_connectionCts = new CancellationTokenSource();
593+
594+
// Store connection info for reconnection
595+
_currentSseUrl = sseUrl;
596+
_currentApiKey = apiKey;
597+
_autoReconnectEnabled = true;
598+
_reconnectAttempts = 0;
587599

588600
// Start listening to SSE events in background
589601
_sseListenerTask = ListenToSseAsync(sseUrl, _connectionCts.Token);
@@ -664,20 +676,55 @@ private static async Task ListenToSseAsync(string url, CancellationToken cancell
664676
}
665677
catch (OperationCanceledException)
666678
{
667-
// Normal disconnection
679+
// Normal disconnection - don't auto-reconnect
680+
_autoReconnectEnabled = false;
668681
}
669682
catch (Exception ex)
670683
{
671684
AnsiConsole.WriteLine();
672-
AnsiConsole.MarkupLine($"[red]SSE connection error:[/] {ex.Message}");
685+
AnsiConsole.MarkupLine($"[red]SSE connection error:[/] {Markup.Escape(ex.Message)}");
673686
}
674687
finally
675688
{
676689
_isConnected = false;
677690
AnsiConsole.WriteLine();
691+
}
692+
693+
// Attempt auto-reconnect if not manually disconnected (moved outside finally)
694+
if (_autoReconnectEnabled && !cancellationToken.IsCancellationRequested && _reconnectAttempts < MaxReconnectAttempts)
695+
{
696+
_reconnectAttempts++;
697+
var delayIndex = Math.Min(_reconnectAttempts - 1, ReconnectDelaysMs.Length - 1);
698+
var delayMs = ReconnectDelaysMs[delayIndex];
699+
700+
AnsiConsole.MarkupLine($"[yellow]Connection lost. Reconnecting in {delayMs / 1000}s (attempt {_reconnectAttempts}/{MaxReconnectAttempts})...[/]");
701+
702+
try
703+
{
704+
await Task.Delay(delayMs, cancellationToken);
705+
706+
if (!cancellationToken.IsCancellationRequested && _currentSseUrl != null)
707+
{
708+
// Reconnect
709+
await ListenToSseAsync(_currentSseUrl, cancellationToken);
710+
return; // Don't show disconnected message
711+
}
712+
}
713+
catch (OperationCanceledException)
714+
{
715+
// Cancelled during reconnect delay
716+
}
717+
}
718+
else if (_reconnectAttempts >= MaxReconnectAttempts)
719+
{
720+
AnsiConsole.MarkupLine("[red]Max reconnection attempts reached. Use 'connect' to reconnect manually.[/]");
721+
}
722+
else if (!_autoReconnectEnabled || cancellationToken.IsCancellationRequested)
723+
{
678724
AnsiConsole.MarkupLine("[yellow]Disconnected from server.[/]");
679-
AnsiConsole.Markup(_isConnected ? "[green]●[/] [green]hookreplay[/]> " : "[red]●[/] [blue]hookreplay[/]> ");
680725
}
726+
727+
AnsiConsole.Markup(_isConnected ? "[green]●[/] [green]hookreplay[/]> " : "[red]●[/] [blue]hookreplay[/]> ");
681728
}
682729

683730
private static async Task HandleSseEvent(string eventType, string data)
@@ -686,6 +733,7 @@ private static async Task HandleSseEvent(string eventType, string data)
686733
{
687734
case "connected":
688735
_isConnected = true;
736+
_reconnectAttempts = 0; // Reset reconnect counter on successful connection
689737
try
690738
{
691739
var connectedEvent = JsonSerializer.Deserialize(data, CliJsonContext.Default.SseConnectedEvent);
@@ -835,9 +883,12 @@ private static async Task ExecuteRequest(ReplayRequest request, string fullUrl)
835883

836884
if (displayBody.Length > 500)
837885
{
838-
displayBody = displayBody[..500] + "\n[silver]... (truncated)[/]";
886+
displayBody = displayBody[..500] + "... (truncated)";
839887
}
840888

889+
// Escape markup characters to prevent Spectre.Console from interpreting them
890+
displayBody = Markup.Escape(displayBody);
891+
841892
AnsiConsole.Write(new Panel(displayBody)
842893
.Header("[silver]Response Body[/]")
843894
.Border(BoxBorder.Rounded)
@@ -859,6 +910,9 @@ private static async Task DisconnectAsync()
859910
return;
860911
}
861912

913+
// Disable auto-reconnect for manual disconnect
914+
_autoReconnectEnabled = false;
915+
862916
await _connectionCts?.CancelAsync();
863917

864918
if (_sseListenerTask != null)

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,35 @@ MIT License - see [LICENSE](LICENSE) for details.
227227

228228
---
229229

230+
## Changelog
231+
232+
### v1.0.17 (2026-01-23)
233+
234+
#### 🐛 Bug Fixes
235+
- **Fixed response body display**: HTML/XML responses with `[` and `]` characters no longer crash the CLI with "malformed markup tag" errors
236+
- **Fixed Spectre.Console markup escaping**: All user-generated content is now properly escaped before rendering
237+
238+
#### ✨ New Features
239+
- **Auto-reconnect**: CLI now automatically reconnects when the connection drops unexpectedly
240+
- Uses exponential backoff: 1s → 2s → 5s → 10s → 30s delays
241+
- Attempts up to 10 reconnections before giving up
242+
- Manual disconnect (`disconnect` command or Ctrl+C) disables auto-reconnect
243+
- Shows clear status messages during reconnection attempts
244+
245+
#### 🔧 Improvements
246+
- Better error messages with escaped content to prevent display issues
247+
- Connection state is properly tracked across reconnection attempts
248+
249+
---
250+
251+
### v1.0.16
252+
253+
- Self-update feature: CLI can update itself without npm/dotnet commands
254+
- Version checking against npm registry on startup
255+
- Direct binary download from GitHub Releases
256+
257+
---
258+
230259
<p align="center">
231260
<strong>Stop chasing webhooks. Start catching bugs.</strong>
232261
<br />

0 commit comments

Comments
 (0)