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; } } }