using Microsoft.AspNetCore.Mvc; namespace DeepDrftManager.Controllers; /// /// Proxies image API calls to DeepDrftAPI so the browser never makes cross-origin requests. /// The CMS host runs server-side only, so rendered image URLs must resolve against the Manager's /// own origin, not the internal API address. This controller forwards unauthenticated /// api/image/{entryKey} requests upstream using the "DeepDrft.Content" named client /// (no API key — the image endpoint is public). /// [ApiController] [Route("api/image")] public class ImageProxyController : ControllerBase { private readonly HttpClient _upstream; private readonly ILogger _logger; public ImageProxyController(IHttpClientFactory httpClientFactory, ILogger logger) { _upstream = httpClientFactory.CreateClient("DeepDrft.Content"); _logger = logger; } /// Proxies image binary streaming by vault entry key from DeepDrftAPI. [HttpGet("{entryKey}")] public async Task GetImage(string entryKey, CancellationToken ct = default) { _logger.LogInformation("Proxying image {EntryKey}", entryKey); var path = $"api/image/{Uri.EscapeDataString(entryKey)}"; HttpResponseMessage upstream; try { upstream = await _upstream.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ct); } catch (Exception ex) { _logger.LogError(ex, "Upstream call to DeepDrftAPI image/{EntryKey} failed", entryKey); return StatusCode(502, "Upstream unavailable"); } if (!upstream.IsSuccessStatusCode) { upstream.Dispose(); _logger.LogWarning("DeepDrftAPI image/{EntryKey} returned {Status}", entryKey, (int)upstream.StatusCode); return StatusCode((int)upstream.StatusCode); } // Do NOT dispose upstream here — File() takes ownership of the response stream // and disposes it after the body is sent. var contentType = upstream.Content.Headers.ContentType?.ToString() ?? "application/octet-stream"; var contentLength = upstream.Content.Headers.ContentLength; if (contentLength.HasValue) Response.ContentLength = contentLength.Value; var stream = await upstream.Content.ReadAsStreamAsync(ct); HttpContext.Response.RegisterForDispose(upstream); return File(stream, contentType, enableRangeProcessing: false); } }