[Perf][ArtStudio] Serve artifact images via short-TTL presigned MinIO URLs + HTTP caching — stop streaming every image through the API (follow-up to #783, epic #413) #784

Closed
opened 2026-07-22 03:41:02 +00:00 by spikerj · 1 comment
Owner

Observed in prod (2026-07-21): Art Studio images load from https://api.spikersoft.com/api/artstudio/{id}/artifacts/{gridFsId}/download — every thumbnail/preview streams through the API, and because the Angular side fetches blobs via the authenticated HttpClient (createObjectURL), the browser HTTP cache never engages. Zero caching, API bandwidth+CPU on every image view. #783 moved the STORE to MinIO but the SERVING path still proxies everything.

Design (house pattern — book covers, BookController.ToDtoWithCover, #618/#719)

Backend

  • ArtAssetArtifactRef gains a transient [BsonIgnore] string? Url — a short-TTL (1h) presigned MinIO GET URL minted per response, after any cache layer, in ArtStudioController via the already-registered keyed IObjectStore("art-asset-artifacts") (null in local dev → field stays null → client falls back to the authenticated download routes; GridFS local-dev path unaffected).
  • Applied on: owner detail + owner list, gallery detail + gallery list (the projections embed ArtAssetArtifactRef, so the field flows through).
  • Presigned URLs carry response-cache-control: private, max-age=3600 via ResponseHeaderOverrides (new optional param on IObjectStore.GetPresignedUrl).
  • The API download routes gain Cache-Control: private, max-age=86400, immutable (artifact content per id is immutable) so even the fallback/blob path caches.
  • Prereq verified: API S3 client signs against public minio.spikersoft.com (documented in the stack file precisely for presigned book covers) and api-svc got rw on the art buckets in infra #138.

Angular

  • ArtAssetArtifactRef model gains url?: string | null; image display paths (shell thumbnails, gallery cards, moderation previews, Photo Stack preview, Touch-Up frame strip) prefer artifact.url in img src and skip the blob fetch entirely.
  • Bearer interceptor is URL-pattern-scoped, so presigned hosts never get an Authorization header (no S3 double-auth clash).

Deliberately OUT of scope

  • Games-handoff manifest URLs stay API download routes — the manifest is an explicit breaking-sensitive contract whose consumers fetch with a bearer token.
  • GLB/mesh viewer + file downloads stay on the authenticated blob path (GLTFLoader fetch needs CORS on MinIO — separate verification; cross-origin download attribute is ignored by browsers).
  • Quarantine previews stay authenticated-only (safety-flagged images, staff review).
Observed in prod (2026-07-21): Art Studio images load from `https://api.spikersoft.com/api/artstudio/{id}/artifacts/{gridFsId}/download` — every thumbnail/preview streams through the API, and because the Angular side fetches blobs via the authenticated HttpClient (`createObjectURL`), the browser HTTP cache never engages. Zero caching, API bandwidth+CPU on every image view. #783 moved the STORE to MinIO but the SERVING path still proxies everything. ## Design (house pattern — book covers, `BookController.ToDtoWithCover`, #618/#719) **Backend** - `ArtAssetArtifactRef` gains a transient `[BsonIgnore] string? Url` — a short-TTL (1h) presigned MinIO GET URL minted **per response, after any cache layer**, in `ArtStudioController` via the already-registered keyed `IObjectStore("art-asset-artifacts")` (null in local dev → field stays null → client falls back to the authenticated download routes; GridFS local-dev path unaffected). - Applied on: owner detail + owner list, gallery detail + gallery list (the projections embed `ArtAssetArtifactRef`, so the field flows through). - Presigned URLs carry `response-cache-control: private, max-age=3600` via `ResponseHeaderOverrides` (new optional param on `IObjectStore.GetPresignedUrl`). - The API download routes gain `Cache-Control: private, max-age=86400, immutable` (artifact content per id is immutable) so even the fallback/blob path caches. - Prereq verified: API S3 client signs against public `minio.spikersoft.com` (documented in the stack file precisely for presigned book covers) and `api-svc` got rw on the art buckets in infra #138. **Angular** - `ArtAssetArtifactRef` model gains `url?: string | null`; image display paths (shell thumbnails, gallery cards, moderation previews, Photo Stack preview, Touch-Up frame strip) prefer `artifact.url` in `img src` and skip the blob fetch entirely. - Bearer interceptor is URL-pattern-scoped, so presigned hosts never get an Authorization header (no S3 double-auth clash). ## Deliberately OUT of scope - **Games-handoff manifest URLs** stay API download routes — the manifest is an explicit breaking-sensitive contract whose consumers fetch with a bearer token. - **GLB/mesh viewer + file downloads** stay on the authenticated blob path (GLTFLoader `fetch` needs CORS on MinIO — separate verification; cross-origin `download` attribute is ignored by browsers). - **Quarantine previews** stay authenticated-only (safety-flagged images, staff review).
Author
Owner

Resolved — both halves merged to master:

  • spikersoft-backend PR #452 (merged 03:50): transient [BsonIgnore] Url on ArtAssetArtifactRef, minted per response post-authorization via the keyed art-asset-artifacts store (1h TTL, book-cover pattern) on owner detail/list, gallery detail/list, submissions + flagged; IObjectStore.GetPresignedUrl gained a responseCacheControl override (MinIO serves private, max-age=3600); API download routes now send Cache-Control: private, max-age=86400, immutable. 119 targeted tests green.
  • spikersoft-angular PR #518 (merged 03:55): image display sites (shell thumbnails/gallery strip/full-size view, gallery cards, moderation previews, photo-stack preview) prefer artifact.url as plain img src with blob-fetch fallback for local dev. GLB loads, file downloads, and touch-up canvas frames deliberately stay on the authenticated blob path; manifest contract and quarantine untouched (documented out-of-scope above). 31/31 spec files green.

Deploys ride the normal master-push pipelines. Closing.

Resolved — both halves merged to master: - **spikersoft-backend PR #452** (merged 03:50): transient `[BsonIgnore] Url` on `ArtAssetArtifactRef`, minted per response post-authorization via the keyed `art-asset-artifacts` store (1h TTL, book-cover pattern) on owner detail/list, gallery detail/list, submissions + flagged; `IObjectStore.GetPresignedUrl` gained a `responseCacheControl` override (MinIO serves `private, max-age=3600`); API download routes now send `Cache-Control: private, max-age=86400, immutable`. 119 targeted tests green. - **spikersoft-angular PR #518** (merged 03:55): image display sites (shell thumbnails/gallery strip/full-size view, gallery cards, moderation previews, photo-stack preview) prefer `artifact.url` as plain `img src` with blob-fetch fallback for local dev. GLB loads, file downloads, and touch-up canvas frames deliberately stay on the authenticated blob path; manifest contract and quarantine untouched (documented out-of-scope above). 31/31 spec files green. Deploys ride the normal master-push pipelines. Closing.
Sign in to join this conversation.