Last active
June 3, 2025 07:17
-
-
Save panicoenlaxbox/cee19a95afe7ca3a376697c1e07167a0 to your computer and use it in GitHub Desktop.
Http utilities
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}\" `"); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}"); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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