diff --git a/DeepDrftPublic.Client/Controls/TrackCard.razor b/DeepDrftPublic.Client/Controls/TrackCard.razor index 18fa9b4..5464481 100644 --- a/DeepDrftPublic.Client/Controls/TrackCard.razor +++ b/DeepDrftPublic.Client/Controls/TrackCard.razor @@ -12,7 +12,7 @@ @if (!string.IsNullOrEmpty(TrackModel?.ImagePath)) { -
+
} else @@ -23,7 +23,7 @@ } else if (!string.IsNullOrEmpty(TrackModel?.ImagePath)) { -
+
} else diff --git a/DeepDrftPublic/Controllers/ImageProxyController.cs b/DeepDrftPublic/Controllers/ImageProxyController.cs new file mode 100644 index 0000000..1dfeeb8 --- /dev/null +++ b/DeepDrftPublic/Controllers/ImageProxyController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; + +namespace DeepDrftPublic.Controllers; + +/// +/// Proxies public image API calls to DeepDrftAPI so the browser never makes +/// cross-origin requests. Mirrors : the WASM +/// client issues relative api/image/{entryKey} requests against this host, +/// which forwards them upstream. SSR prerender calls DeepDrftAPI directly via the +/// same named client — no proxy hop needed on the server side. +/// +[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.API"); + _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); + } +}