Skip to content

Instantly share code, notes, and snippets.

@panicoenlaxbox
Last active June 3, 2025 07:17
Show Gist options
  • Save panicoenlaxbox/cee19a95afe7ca3a376697c1e07167a0 to your computer and use it in GitHub Desktop.
Save panicoenlaxbox/cee19a95afe7ca3a376697c1e07167a0 to your computer and use it in GitHub Desktop.
Http utilities
using System.Net.Http.Headers;
using System.Text;
namespace ConsoleApp1;
public class CurlHandler(string path, IEnumerable<string> redactedHeaders) : DelegatingHandler
{
private static readonly Lock FileLock = new();
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var content = new StringBuilder();
content.AppendLine($"curl -X {request.Method} \"{request.RequestUri}\" `");
WriteHeaders(request.Headers, content);
if (request.Content is not null)
{
WriteHeaders(request.Content.Headers, content);
var originalStream = await request.Content.ReadAsStreamAsync(cancellationToken);
var memoryStream = new MemoryStream();
await originalStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(memoryStream, leaveOpen: true);
var requestContent = await reader.ReadToEndAsync(cancellationToken);
content.AppendLine($"-d '{requestContent}'");
memoryStream.Seek(0, SeekOrigin.Begin);
var newContent = new StreamContent(memoryStream);
foreach (var header in request.Content.Headers)
{
newContent.Headers.Add(header.Key, header.Value);
}
request.Content = newContent;
}
content.AppendLine();
lock (FileLock)
{
File.AppendAllText(path, content.ToString());
}
return await base.SendAsync(request, cancellationToken);
}
private void WriteHeaders(HttpHeaders headers, StringBuilder stringBuilder)
{
foreach (var header in headers)
{
if (redactedHeaders.Contains(header.Key, StringComparer.OrdinalIgnoreCase))
{
stringBuilder.AppendLine($"-H \"{header.Key}: *\" `");
continue;
}
stringBuilder.AppendLine($"-H \"{header.Key}: {string.Join(" ", header.Value)}\" `");
}
}
}
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace ConsoleApp1;
public class HttpFileHandler(string path, IEnumerable<string> redactedHeaders) : DelegatingHandler
{
private static readonly Lock FileLock = new();
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var content = new StringBuilder();
content.AppendLine($"{request.Method} {request.RequestUri}");
WriteHeaders(request.Headers, content);
if (request.Content is not null)
{
WriteHeaders(request.Content.Headers, content);
var originalStream = await request.Content.ReadAsStreamAsync(cancellationToken);
var memoryStream = new MemoryStream();
await originalStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Seek(0, SeekOrigin.Begin);
using var requestReader = new StreamReader(memoryStream, leaveOpen: true);
var requestContent = await requestReader.ReadToEndAsync(cancellationToken);
if (request.Content.Headers.ContentType?.MediaType == MediaTypeNames.Application.Json)
{
var jsonElement = JsonSerializer.Deserialize<JsonElement>(requestContent);
requestContent = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
content.AppendLine($"{Environment.NewLine}{requestContent}");
memoryStream.Seek(0, SeekOrigin.Begin);
var newContent = new StreamContent(memoryStream);
foreach (var header in request.Content.Headers)
{
newContent.Headers.Add(header.Key, header.Value);
}
request.Content = newContent;
}
content.AppendLine();
var response = await base.SendAsync(request, cancellationToken);
content.AppendLine("###");
content.AppendLine();
WriteHeaders(response.Headers, content, "# ");
WriteHeaders(response.Content.Headers, content, "# ");
var skipBodyDump =
response.Headers.TransferEncodingChunked == true ||
response.Content.Headers.ContentType?.MediaType == MediaTypeNames.Text.EventStream;
if (!skipBodyDump)
{
// ReadAsStringAsync() buffers the content internally, so it won’t interfere with other consumers of response.Content, unlike ReadAsStreamAsync() which consumes the stream directly
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
responseContent = string.Join(Environment.NewLine,
responseContent.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(line => "# " + line));
content.AppendLine(responseContent);
content.AppendLine();
}
else
{
content.AppendLine("# Chunked or streaming response detected. Skipping body dump.");
content.AppendLine();
}
lock (FileLock)
{
File.AppendAllText(path,
$"{(File.Exists(path) ? $"###{Environment.NewLine}{Environment.NewLine}{content}" : content.ToString())}");
}
return response;
}
private void WriteHeaders(HttpHeaders headers, StringBuilder content, string? prefix = null)
{
foreach (var header in headers)
{
if (redactedHeaders.Contains(header.Key, StringComparer.OrdinalIgnoreCase))
{
content.AppendLine($"{prefix}{header.Key}: *");
continue;
}
content.AppendLine($"{prefix}{header.Key}: {string.Join(" ", header.Value)}");
}
}
}
using ConsoleApp1;
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace Microsoft.Extensions.DependencyInjection;
#pragma warning restore IDE0130 // Namespace does not match folder structure
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddHttpHandlers(this IServiceCollection services)
{
const string httpFilePath = "ConsoleApp1.http";
if (File.Exists(httpFilePath))
{
File.Delete(httpFilePath);
}
string[] redactedHeaders = [];
services.AddTransient(_ => new HttpFileHandler(httpFilePath, redactedHeaders));
const string ps1FilePath = "ConsoleApp1.ps1";
if (File.Exists(ps1FilePath))
{
File.Delete(ps1FilePath);
}
services.AddTransient(_ => new CurlHandler(ps1FilePath, redactedHeaders));
services.AddHttpClient().ConfigureHttpClientDefaults(cfg => cfg
.AddHttpMessageHandler<HttpFileHandler>()
.AddHttpMessageHandler<CurlHandler>()
);
return services;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment