Skip to content

fix(client): adopt the runner's re-scoped CDN token from the queue result - #1136

Draft
hanchchch wants to merge 1 commit into
mainfrom
hanch/fal-client-adopt-response-cdn-token
Draft

fix(client): adopt the runner's re-scoped CDN token from the queue result#1136
hanchchch wants to merge 1 commit into
mainfrom
hanch/fal-client-adopt-response-cdn-token

Conversation

@hanchchch

Copy link
Copy Markdown
Collaborator

Problem

handle.get() fetched the queue result and returned response.json() without looking at the headers, so the queue path silently dropped the x-fal-cdn-token the runner returns. subscribe is built on submit + handle.get(), so every chaining app using it lost the token.

submit does call handle_response_headers — but on the enqueue response, written before the runner has even run. Its scope can never cover the request ids the run ended up with access to. Only the result response can.

Why it matters

Once CDN tokens are request-scoped, an inner app's output is tagged with the inner request id, so the outer app's own token grants no read on it. The design ("CDN Access Control", Request-Scoped Access → Flow step 4) handles this by having the runner return a token whose scope accumulated every hop, which the gateway forwards upstream. That's needed rather than gateway-side reconstruction because in an A→B→C chain the gateway only knows the hop it proxied.

Everything except this step is already in place:

leg mechanism status
gateway → runner (extend scope) _get_cdn_token
runner → response (set current token) fal.App middleware
gateway → caller (forward worker token) _build_worker_response_headers — worker's token overrides the platform default
caller adopts returned token handle_response_headers run/stream, ✗ queue path

Verified against prod by forwarding a token as the caller: both fal.run and the queue result endpoint return x-fal-cdn-token, scoped read:request_id:<that run's id>.

Change

One call in each of SyncRequestHandle.get() and AsyncRequestHandle.get(), after _raise_for_status.

handle_response_headers is already a no-op outside a fal app context, so non-app SDK users are unaffected.

Testing

TestQueueResultAdoptsCdnToken — sync and async adoption, no-token-in-response leaves the existing token untouched, and no-app-context does not raise.

  • pytest tests/unit → 148 passed
  • pre-commit run --files ... → clean (ruff, ruff-format)

Not covered — fal.apps has no token handling at all

fal.apps.run/submit send only creds.to_headers(): they neither forward x-fal-cdn-token on the way out nor adopt it on the way back. This PR does not reach them.

Four registry callers use that path:

caller chains to exposure
registry/video/fast_svd.py fal-ai/fast-sdxl intermediate image ends up owned by the app owner and public; the parent then reads it by URL
registry/video/fast_svd_lcm.py fal-ai/fast-sdxl same
registry/fine_tuning/personalization/inference.py fal-ai/moondream/batched text output, no CDN object — unaffected
registry/image/supir/inference.py fal-ai/llava-next text output, no CDN object — unaffected

The two fast-sdxl chains keep working post-flip precisely because the intermediate is not caller-scoped, but the caller does not own their own intermediate and it is world-readable. Suggested follow-up is migrating those callers to registry.client (which forwards, see fal-ai/registry#12943) rather than teaching fal.apps the protocol — one less client to maintain.

Part of INFRA-4420.

…sult

``handle.get()`` fetched the result and returned ``response.json()`` without
looking at the headers, so the queue path silently dropped the
``x-fal-cdn-token`` the runner returns. Since ``subscribe`` is built on
``submit`` + ``handle.get()``, every chaining app that uses it lost the token.

The result response is the only one that can carry it. ``submit`` already calls
``handle_response_headers``, but on the *enqueue* response -- written before the
runner has run -- so its scope can never cover the request ids the run ended up
with access to.

This matters once CDN tokens are request-scoped: an inner app's output is tagged
with the *inner* request id, so the outer app's own token does not grant read on
it. The runner returns a token whose scope accumulated every hop, the gateway
forwards it upstream (``_build_worker_response_headers`` treats the worker's
token as overriding the platform default), and the caller has to adopt it. That
last step was missing here. Verified against prod: both ``fal.run`` and the
queue result endpoint return ``x-fal-cdn-token`` with
``read:request_id:<that run's id>``.

Design: "CDN Access Control", Request-Scoped Access / Flow step 4.
@linear-code

linear-code Bot commented Aug 11, 2026

Copy link
Copy Markdown

INFRA-4420

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