using System.Text;
namespace AIStudio.Tools.Web;
///
/// Reads an HTTP response body as text, without trusting what it claims about its size.
///
public static class HttpContentReader
{
private const int CHUNK_SIZE = 8192;
///
/// Reads the body as text, refusing anything beyond the given limit.
///
///
/// The declared content length is checked first, and the actual bytes are counted while
/// reading — a server may understate the length or omit it entirely. Counting happens after
/// decompression, so a small compressed body that expands into a large one is caught too.
///
/// The response body.
/// The most that may be read.
/// The cancellation token.
/// The body as text, decoded by its declared charset or UTF-8.
/// The body exceeds the limit.
public static async Task ReadAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
{
if (content.Headers.ContentLength is { } contentLength && contentLength > maxResponseBytes)
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
await using var stream = await content.ReadAsStreamAsync(token);
await using var buffer = new MemoryStream();
var chunk = new byte[CHUNK_SIZE];
while (true)
{
var read = await stream.ReadAsync(chunk, token);
if (read is 0)
break;
if (buffer.Length + read > maxResponseBytes)
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
buffer.Write(chunk, 0, read);
}
return (TryGetContentEncoding(content) ?? Encoding.UTF8).GetString(buffer.ToArray());
}
private static Encoding? TryGetContentEncoding(HttpContent content)
{
var charset = content.Headers.ContentType?.CharSet?.Trim();
if (string.IsNullOrWhiteSpace(charset))
return null;
try
{
return Encoding.GetEncoding(charset.Trim('"'));
}
catch
{
// An unknown or malformed charset is not worth failing the request over; the caller
// falls back to UTF-8, which is what such a server almost always meant.
return null;
}
}
}