fix(upload): point a blocked browser upload at bucket CORS, not the API - #66
Merged
Conversation
railway-app
Bot
temporarily deployed
to
vcsk-railway-pilot / production
August 13, 2026 17:33
Inactive
EduPav
force-pushed
the
fix/api-boots-from-service-root
branch
from
August 13, 2026 20:35
a551d7b to
7f8b11f
Compare
Uploads go browser -> B2 via a presigned PUT, so when the bucket's CORS does not allow the deployed origin the browser blocks the request and XHR reports a contentless `error` event. That handler reused `networkError()`, whose message reads "Couldn't reach the API ... Check the API logs" — the one place that looks perfectly healthy, because the presign that produced the URL returned 200. The message sent people to the wrong system for a failure with a one-line fix. `storageNetworkError()` now handles the storage leg and names the actual cause and remedy (the bucket's CORS, and the script that sets it). The offline branch is kept: `navigator.onLine === false` still reports a connectivity problem. The Railway delivery contract never mentioned bucket CORS at all — it documented only `API_CORS_ORIGINS`, which is a different layer. The requirement is not platform-specific: it follows from the direct-to-B2 upload flow, so it applies to every deploy target. It is now a named subsection there, alongside two verification steps the pilot proved were needed: - a deployment status of `SUCCESS` is not evidence the app runs — a service with no configured health check reports success as soon as the container starts, so it can crash-loop behind a green deployment - smoke-test an upload from a *browser*, not only with curl: a server-side request bypasses CORS entirely, so every scripted check passes while real users cannot upload The regression test stubs XMLHttpRequest, fires the blocked-PUT `error` event, and asserts the message names bucket CORS and does *not* mention the API logs.
EduPav
force-pushed
the
fix/surface-bucket-cors-requirement
branch
from
August 13, 2026 20:39
e08dc4e to
93ba30a
Compare
EduPav
marked this pull request as ready for review
August 13, 2026 20:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Uploads go browser → B2 via a presigned PUT. When the bucket's CORS does not allow the
deployed origin, the browser blocks the request before it leaves and XHR reports a
contentless
errorevent — no status, no body.That handler reused
networkError(), which says:The API is the one component that is fine. The presign that produced the URL returned 200 and
logged cleanly, so anyone following that advice searches a healthy log for a failure that
happened somewhere else entirely. For a problem whose fix is a single command, the error
message was actively costing debugging time.
storageNetworkError()now handles the storage leg and names the cause and the remedy. Theoffline branch is preserved —
navigator.onLine === falsestill reports connectivity.The docs gap
The Railway delivery contract never mentioned bucket CORS. It documented
API_CORS_ORIGINS,which is a different layer: that one is the API allowing the web origin, and it is only
needed where web and API are separate origins. Bucket CORS is B2 allowing the browser, and it
follows from the direct-to-B2 upload flow, so it applies to every deploy target — the README
already notes it for Vercel; Railway had nothing.
It is now a named subsection in
infra/railway/README.md, with the merge-not-replace helperinvocation, plus two verification steps this pilot proved were needed:
SUCCESSis not evidence the app runs. A service with no configuredhealth check reports success as soon as the container starts, so it can crash-loop behind a
green deployment. Confirm the deployed commit and read the deploy logs.
CORS entirely, so the bucket rule can be missing while every scripted check passes.
How this was found
Deploying the kit for real.
/healthreportedb2_connected: true, the UI loaded, listingworked, an end-to-end round trip driven by
curlpassed all six steps — and the browser uploadpath was dead. A preflight probe against the presigned URL returned 403 with no
Access-Control-Allow-Originbefore the bucket rule was added, and 200 with the exact originand
allow-methods: PUTafter. Same code, same credentials; the only difference was a rule onthe bucket.
Worth stating plainly: this affects the already-shipped Vercel deploy button the same way.
Any one-click deploy of this repo currently lands a deployer in a healthy-looking app whose
uploads fail silently until they add a CORS rule for their origin.
Checks run by Claude
pnpm verify— green, exit 0:check:agent-docs109 checks, backend ruff + pytest +structure, frontend lint, 164 unit tests, typecheck and production build
XMLHttpRequest, fires the blocked-PUTerrorevent, and asserts themessage names bucket CORS and does not mention the API logs; it also asserts
verifyis never called after a failed PUT
networkError()is untouched for the APIfetch()path, where its wording is correctManual checks
None outstanding.