Skip to content

fix(fetch): call linked stdlib fetch constructors directly under external-fetch-symbols#5687

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix-fetch-registry-extern
Jun 25, 2026
Merged

fix(fetch): call linked stdlib fetch constructors directly under external-fetch-symbols#5687
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix-fetch-registry-extern

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

The runtime's fetch-constructor wrappers in crates/perry-runtime/src/object/global_fetch.rscall_global_headers_new, call_global_request_new, and the Blob/File/Response siblings — call into perry-stdlib indirectly through the GLOBAL_FETCH_* AtomicPtr registry, which js_stdlib_init_dispatch (js_register_global_fetch_constructors) populates at program entry. When a wrapper loads its registry pointer and sees null, it warns

[perry] warning: `js_headers_new` is a no-op stub on this platform — fetch symbol from perry-stdlib not linked into this binary (runtime-only build)

and returns undefined, so the construction no-ops.

These wrappers are only reached by the registry-backed construction path — a reflective/aliased new Headers() / new Request() resolved through the class registry (object/class_registry/construct.rs), e.g. const K = Headers; new K(). A literal new Headers() takes the codegen fast path, which emits a direct js_headers_new call and is unaffected.

In a large, auto-optimized binary that bundles its own runtime copies, the registry pointer could be observed null even though init had already stored it — so the reflective path no-opped (and a subsequent fetch() hung) while the equivalent direct codegen path worked on the very same symbol.

Fix

call_fetch_with_options already handles exactly this: under the external-fetch-symbols feature it calls the linked js_fetch_with_options extern directly instead of going through the registry, because in that build perry-stdlib's web-fetch implementations are statically linked. This PR extends that established pattern to the nine fetch-constructor wrappers:

  • #[cfg(feature = "external-fetch-symbols")] → call the linked js_blob_new / js_file_new / js_headers_new / js_headers_init_from_value / js_request_new / js_response_new / js_response_static_json / js_response_static_redirect / js_response_static_error externs directly.
  • #[cfg(not(feature = "external-fetch-symbols"))] → unchanged: registry load + stub-warn, for runtime-only builds where these externs are not linked and a runtime stub provides the symbol.

This makes the registry construction path consistent with both the codegen fast path and call_fetch_with_options, and sidesteps the registry-visibility hazard entirely.

Why it's link-safe

external-fetch-symbols is enabled exactly when the program uses fetch, which links perry-stdlib with web-fetch. web-fetch provides all nine symbols (src/fetch/ plus src/fetch_blob.rs), so they are guaranteed resolvable whenever the feature is on — the same invariant call_fetch_with_options already depends on.

Testing

  • cargo check -p perry-runtime (default / stub build) — clean.
  • cargo check -p perry-runtime --features external-fetch-symbols (direct-extern build) — clean.
  • No behavior change for the default (stub) build; the not(external-fetch-symbols) variants are byte-for-byte the previous code.

Summary by CodeRabbit

  • New Features

    • Improved support for runtime fetch-related web objects in builds that use statically linked web APIs.
    • Added a fallback path for environments where those symbols are not available.
  • Bug Fixes

    • Prevented uninitialized fetch constructors from failing silently by returning safe empty values and showing a clearer warning.

…rnal-fetch-symbols

The runtime's fetch-constructor wrappers (`call_global_headers_new`,
`call_global_request_new`, and the Blob/File/Response siblings in
`global_fetch.rs`) route through the `GLOBAL_FETCH_*` AtomicPtr registry that
perry-stdlib populates from `js_stdlib_init_dispatch`. When a wrapper observes
its registry pointer null it warns (`js_headers_new is a no-op stub ...`) and
returns `undefined`.

In an `external-fetch-symbols` build, perry-stdlib's `web-fetch` implementations
are statically linked — the same invariant `call_fetch_with_options` already
relies on to call `js_fetch_with_options` directly, and the same one the codegen
fast path uses when it emits a direct `js_headers_new` for a literal
`new Headers()`. Only the registry-backed construction path (a reflective
`new Headers()` / `new Request()` resolved through the class registry, e.g. an
aliased `const K = Headers; new K()`) still went through the AtomicPtr. In a
large auto-optimized binary with bundled runtime copies that wrapper could read
the pointer null even though init had already stored it, yielding a no-op fetch
(a hang on the first request) while the equivalent direct codegen path worked.

Extend the existing `external-fetch-symbols` direct-call pattern to the nine
fetch-constructor wrappers so the registry path matches the codegen path: under
the feature they call the linked `js_*` externs directly; without it they keep
today's registry-load + stub-warn behavior for runtime-only builds. No behavior
change for the default (stub) build.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2294bfb2-ca96-4375-b975-684b560abbf2

📥 Commits

Reviewing files that changed from the base of the PR and between 31a1983 and 06ee690.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/object/global_fetch.rs

📝 Walkthrough

Walkthrough

The runtime now splits fetch-related global constructors between registry-backed lookups and direct extern "C" thunks when external-fetch-symbols is enabled. The stub warning helper is compiled only for the registry-backed path, and blob, file, headers, request, and response constructors follow the new split.

Changes

External fetch symbol constructors

Layer / File(s) Summary
Feature gate and basic constructors
crates/perry-runtime/src/object/global_fetch.rs
The runtime-only warning path is gated off for external-symbol builds, external constructor thunks are declared, and blob, file, headers, and headers-init wrappers split between registry lookup and direct calls.
Request constructor split
crates/perry-runtime/src/object/global_fetch.rs
The request constructor wrapper now calls js_request_new directly when external-fetch-symbols is enabled.
Response constructors
crates/perry-runtime/src/object/global_fetch.rs
The response constructor and static JSON wrappers now call js_response_new and js_response_static_json directly under the feature flag.
Redirect and error helpers
crates/perry-runtime/src/object/global_fetch.rs
The redirect and error response helpers now call js_response_static_redirect and js_response_static_error directly under the feature flag.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • PerryTS/perry#5189: Also changes the build-time wiring for Web Fetch globals and their constructor availability.
  • PerryTS/perry#5679: Shares the external-fetch-symbols feature flag and changes how fetch-related symbols are linked and resolved.

Poem

🐇 I hop through fetch paths, neat and bright,
One paw for stubs, one paw for symbols light.
Blob and response now twinkle in line,
Direct from the thunks, all calm and fine.
✨ Carrots compiled; the runtime sings tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: direct linked stdlib fetch constructor calls under external-fetch-symbols.
Description check ✅ Passed The description covers the problem, fix, link-safety, and testing, though it omits some template sections like related issue and checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Verified end-to-end against a large minified CLI bundle whose startup constructs Headers/Request reflectively (an aliased new K()), which routes through the class-registry construction path rather than the codegen fast path:

  • Before: stderr emits `js_headers_new` is a no-op stub … and `js_request_new` is a no-op stub …, then the process hangs on the first fetch() (the no-op fetch never resolves).
  • After: zero no-op-stub warnings; the binary now establishes real outbound TLS connections during startup — i.e. the fetch actually executes — instead of no-op-ing and hanging.

Also confirmed cargo check -p perry-runtime is clean both with and without external-fetch-symbols, and the not(external-fetch-symbols) (default stub build) variants are byte-identical to the previous code.

@proggeramlug proggeramlug merged commit 49ea13f into PerryTS:main Jun 25, 2026
15 checks passed
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