Skip to content

fix(upload): point a blocked browser upload at bucket CORS, not the API - #66

Merged
EduPav merged 1 commit into
mainfrom
fix/surface-bucket-cors-requirement
Aug 13, 2026
Merged

fix(upload): point a blocked browser upload at bucket CORS, not the API#66
EduPav merged 1 commit into
mainfrom
fix/surface-bucket-cors-requirement

Conversation

@EduPav

@EduPav EduPav commented Aug 13, 2026

Copy link
Copy Markdown
Member

Third in the stack: #64#65 → this. Bases retarget automatically as each merges.

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 error event — no status, no body.

That handler reused networkError(), which says:

Couldn't reach the API, or the server returned an error the browser blocked (CORS). Check the API logs.

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. The
offline branch is preserved — navigator.onLine === false still 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 helper
invocation, plus two verification steps this 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. Confirm the deployed commit and read the deploy logs.
  • Smoke-test an upload from a browser, not only with curl. A server-side request bypasses
    CORS entirely, so the bucket rule can be missing while every scripted check passes.

How this was found

Deploying the kit for real. /health reported b2_connected: true, the UI loaded, listing
worked, an end-to-end round trip driven by curl passed all six steps — and the browser upload
path was dead. A preflight probe against the presigned URL returned 403 with no
Access-Control-Allow-Origin
before the bucket rule was added, and 200 with the exact origin
and allow-methods: PUT
after. Same code, same credentials; the only difference was a rule on
the 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 verifygreen, exit 0: check:agent-docs 109 checks, backend ruff + pytest +
    structure, frontend lint, 164 unit tests, typecheck and production build
  • the new test stubs XMLHttpRequest, fires the blocked-PUT error event, and asserts the
    message names bucket CORS and does not mention the API logs; it also asserts verify
    is never called after a failed PUT
  • networkError() is untouched for the API fetch() path, where its wording is correct

Manual checks

None outstanding.

@EduPav EduPav self-assigned this Aug 13, 2026
@railway-app
railway-app Bot temporarily deployed to vcsk-railway-pilot / production August 13, 2026 17:33 Inactive
@EduPav
EduPav force-pushed the fix/api-boots-from-service-root branch from a551d7b to 7f8b11f Compare August 13, 2026 20:35
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
EduPav force-pushed the fix/surface-bucket-cors-requirement branch from e08dc4e to 93ba30a Compare August 13, 2026 20:39
@EduPav
EduPav changed the base branch from fix/api-boots-from-service-root to main August 13, 2026 20:39
@EduPav EduPav closed this Aug 13, 2026
@EduPav EduPav reopened this Aug 13, 2026
@EduPav
EduPav marked this pull request as ready for review August 13, 2026 20:50
@EduPav
EduPav merged commit cf2431a into main Aug 13, 2026
3 checks passed
@EduPav
EduPav deleted the fix/surface-bucket-cors-requirement branch August 13, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant