[Bug][Angular][Backend] Lesson-video and geography-media URLs are root-relative, so the browser asks nginx (learn.spikersoft.com) — which serves index.html with a 200 into the <video> element, never the media #599
Reference in New Issue
Block a user
Found while verifying #528. Verified against production with live HTTP probes, not inferred.
The bug
The backend mints these media URLs as root-relative strings and persists them:
LessonVideoProcessor/Services/LessonVideoMoveConsumer.cs:132-133→/lesson-videos/{username}/{name}.mp4(+.jpgthumbnail). No config override exists — always root-relative.Business/Domain/Geography/Commands/UploadGeographyMedia/UploadGeographyMediaCommandHandler.cs:51→/geography-media/{uniqueName}.Angular binds them raw, with no API base prepended:
libraries/platform/clang-runtime/src/lib/playground-videos-pane.component.html:18→<video [src]="video.url">projects/spikersoft/src/app/_components/admin/lesson-video-moderation/lesson-video-moderation.component.html:44projects/spikersoft/src/app/_components/learning/geography-explorer/country-detail/country-detail.component.html:71,77→<img/video [src]="currentMedia.url">The SPA is served from learn.spikersoft.com, so a root-relative URL resolves against nginx, not
api.spikersoft.com. But nginx's root is/usr/share/nginx/htmlwhile the media tree is bind-mounted athtml/spikersoft/(spikersoft-angular/docker-stack.yml:10), andnginx.confighas no/lesson-videosor/geography-medialocation — only/spikersoft/*and atry_files $uri $uri/ /index.htmlcatch-all.Proof (live, today)
The lesson-video case is the nasty one: HTTP 200 with
text/html. The<video>element is handed the SPA shell as its media payload. It fails silently — no console 404, no network error, just a player that never plays.Impact
/Lessons/{n}/videosreturns empty), so nobody has hit it yet. It breaks the instant the first video is approved. Note lesson-video-processor has also never deployed (#581) — these two will surface together./geography-media/...URL loaded by the SPA.The API's own
/lesson-videosand/geography-mediastatic mounts (and the #528 S3 fallback behind them) are correct and working — they are simply never addressed, because no consumer ever makes the URL absolute against the API origin.Fix options
${environment.baseUrl}${url}) — this is what blog media already does inblog-entry-component.ts:108-118, so there is an established pattern. Cheapest, no data migration.BlogMedia:PublicUrlPrefix) — but this bakes an origin into Mongo rows and needs a backfill for existing data.locationblocks — rejected: it duplicates the serving topology the #413 epic is actively trying to collapse.Option 1 is recommended, and it composes cleanly with #528 (the API already serves both routes correctly, from disk today and from S3 after cutover).
Acceptance
<video>/<img>for lesson videos and geography media resolve against the API origin.text/html.Related: #528 (API media serving), #531 (nginx root bind), #581 (lesson-video-processor never deployed). This is the discrepancy
README-AUDIT-QUESTIONS.md:250was chasing: "Who serves /lesson-videos in production today?" — Answer: nobody.Resolved in spikersoft-angular PR #195 (merged to
master) — the recommended option 1. AmediaUrl()helper prependsenvironment.baseUrlto root-relative media URLs in all three consuming surfaces (playground-videos-pane,lesson-video-moderation, geographycountry-detail), mirroring the blog-media pattern; absolute CDN/S3 URLs pass through untouched. A spec pins that lesson-video URLs are never left root-relative (acceptance bullet 2). Verified:platform-clang-runtime54/54, the two app component suites 25/25.Remaining nicety from acceptance bullet 3 (e2e/route-walk check that a media response is not
text/html) was not included — it needs an approved lesson video in the environment to be meaningful (none exist yet, see #581). If that coverage is still wanted once lesson-video-processor deploys, it can ride along with #581's verification. Closing.