A Form on `site.example.com` can now send a Resource request to `api.example.com`. Cloudflare fetches use public routing, while requests back into the same app are stopped even when they arrive through a different hostname. ## Checklist - [x] Enable public `fetch()` routing in the self-hosted Cloudflare Worker and Pages templates. - [x] Narrow Cloudflare Form Resource hostname checks to site hostnames, including published aliases; keep the Node transport policy intact. - [x] Explain the routing and recursion boundary in code comments. - [x] Add focused regression tests and verify they fail before the fix and pass afterward. - [x] Reject Cloudflare Resource requests that re-enter the app through an alias, and verify both Worker and Pages paths. - [x] Run relevant package tests, typechecks, lint, and fixture builds. - [x] Review the authoritative `docs/` content. No user guidance currently describes this hostname policy, so no docs change is needed. The `global_fetch_strictly_public` flag affects all Cloudflare `fetch()` calls in the generated app. It routes same-zone requests through the public front door. The known site hostnames are blocked before fetch; a reserved request marker carrying the originating project ID stops direct reentry through any additional self-hosted alias while allowing other projects. The marker is set on every redirect hop. Verification: SDK suite (914 tests), CLI prebuild suite (143 tests), SDK and CLI typechecks, published types, lint, package boundaries, formatting, full fixture pipeline, and both Cloudflare fixture typechecks/builds passed. The new marker test failed before the fix and passed afterward. Local Pages returned 508 for a marked request and 200 for an ordinary request. Local Worker returned 508 for a marked request; an ordinary local request hit a fixture-only `assets/process` module error. The optional local fixture-size check lacked the unrelated React Router Netlify built client artifact; the prior PR CI fixture-size check passed. Staging test: moved from #6621 to `frm.staging`. The Builder deployed at https://frm.staging.webstudio.is/ and the Cloudflare template artifact build and validation passed. - [x] Publish the test site from staging and verify a Resource call to a sibling API hostname, plus recursive-call protection. Live staging verification: Published `ee-trpc-test-ivan.wstd.work` from Builder commit `35d22fc8a`. A temporary Resource to `api.wstd.work` produced that host's HTTP 401 response in published page data, confirming the request reached the sibling host. A marked request initially returned 200 because the SaaS publish Worker uses a separate entrypoint. The matching guard is in webstudio-is/webstudio-saas#673; deploying that branch made the marked request return 508 while an ordinary request returned 200. The temporary Resource and binding were removed and the clean build was republished with the SaaS fix. ## Resource response limit follow-up - [x] Enforce 1 MiB (1,048,576 decoded bytes) for all HTTP Resource responses, including page Resources, dependencies, Form actions, and cache hits; reject before cache writes. - [x] Cancel oversized responses and report `RESPONSE_TOO_LARGE` without retrying. Show: “Resource response exceeds 1 MiB. Request fewer fields or use pagination.” - [x] Verify exact boundaries, streamed overflow, compressed responses, cache behavior, and Form/loader error propagation with regression tests. - [x] Update Resource and Form documentation to explain the limit and recovery. - [x] Monitor CI after pushing the verified implementation. All checks passed for `f59b770d45112581514eaa8bc42f57b7f27d290b`, including all ten browser E2E jobs, visual regression, Linux/Windows fixtures, and staging builds/deployments. Verified locally: all 926 SDK tests, SDK typecheck and published types, lint, package boundaries, Form browser boundary, formatting, full fixture pipeline, and Cloudflare React Router fixture typecheck/build passed. All 12 new tests and the updated transport test failed against the previous implementation; disabling only the retry guard also reproduced a duplicate attempt. A local Cloudflare runtime test with gzip accepted exactly 1 MiB of decoded data and rejected one byte over. The full fixture sync pulled an unrelated saved-project text change; the original fixture source was restored and regenerated, leaving no fixture diff. The response limit is enforced before JSON parsing and before Resource cache writes, and checked again when loading cache hits. Oversized responses return `RESPONSE_TOO_LARGE` with `retryable: false`; the graph honors that flag. System and Email Resources retain their own limits. The implementation adds 59 net non-test code lines plus 5 documentation lines, below the 70–100 estimate. Browser E2E passed in CI. Agent evaluations were not run; they require separate permission and were not needed for this transport change. ## Simplification follow-up - [x] Reuse Content Engine's bounded byte reader for Resource bodies and Form uploads; remove duplicate accumulation and cancellation loops (estimate: about 40 fewer production lines). - [x] Preserve response limits, request limits, cancellation, error messages, and retry behavior; run existing SDK and Content Engine coverage plus package/build checks. - [x] Review documentation impact; behavior and approved copy should remain unchanged. - [x] Push the simplification and verify CI. All checks passed for `755ec4210a7858fe298ad4d3a725a0167fb5bb26`, including all ten browser E2E jobs and staging deployment checks. The simplification removes 37 net non-test lines, reducing the response-limit implementation from 59 added lines to 22. Content Engine now exposes its existing byte-stream module directly and accepts a caller-provided limit error. Resource bodies and Form uploads reuse that reader. Native Response construction also replaces redundant status/header copying. All existing tests remain. Verification: 926 SDK tests and 655 Content Engine tests passed, along with both packages' typechecks/builds, published types, lint, package boundaries, Form browser boundary, formatting, the full fixture pipeline, and a Cloudflare fixture build. Local Cloudflare runtime checks passed for gzip at/over the limit and empty 204/205/304 responses. The generated fixture vendor change is limited to the shared reader's optional error argument. Documentation and approved copy remain accurate and unchanged. ## Response handling cleanup - [x] Share bounded response reconstruction between protected fetches and the cache; remove stale encoding/length headers after decoding (estimate: 5–10 fewer non-test lines). - [x] Reproduce the protected-response header bug, then verify the fix, limits, empty responses, and existing SDK coverage. - [x] Review documentation and comments for accurate scope; preserve approved user-facing copy. - [x] Run relevant checks, regenerate fixtures, push, and monitor CI. The shared response reader fixes stale compression and length headers in protected fetches and removes four net production lines (slightly below the 5–10 estimate). All 927 SDK tests, types, published types, lint, package boundaries, and Form browser boundary passed. The new regression test failed on the old code and passed after the fix. Cloudflare runtime checks passed for decoded bodies and empty 204/205/304 responses. The existing Resource/Form documentation remains accurate; approved copy is unchanged. The reentry marker now carries the originating project ID. A different project can serve the request; the same project rejects direct reentry through any hostname. This is cooperative loop detection, not a sandbox for arbitrary code. - [x] Reproduce and fix overflow and redirect hangs when a response has an unread clone; preserve stream cancellation and error handling for all shared-reader callers. The clone regressions failed before the fixes and passed afterward. Cancellation still starts immediately and releases the reader lock, but no longer waits for an unread tee branch. Both overflow rejection and redirect following are covered. All 929 SDK tests and 655 Content Engine tests passed, along with both typechecks and lint. A local Cloudflare runtime also rejected a cloned oversized response promptly. Combined with response reconstruction reuse, this cleanup removes five net production lines. Fixture regeneration changes only the bundled cancellation logic. Final verification for `7761f67d6d1e0a853a6ef239f482ebaf9d70c1ae`: all CI checks passed, including all ten browser E2E shards, visual regression, package/Builder/CLI tests, size checks, Linux and Windows fixtures, and staging deployment workflows. Full fixture regeneration and the Cloudflare fixture build also passed locally. No agent evaluations were run; they require separate permission and were not needed for these fixes. ## Scope reentry protection to the project - [x] Replace the global marker with the originating project ID and compare it with generated project metadata before handling requests, including cached requests. - [x] Cover unrelated sites, same-project aliases, and redirects with regression tests; demonstrate failure before the fix. - [x] Review documentation and clarify that this is cooperative loop detection, not a sandbox for untrusted code. - [x] Regenerate fixtures, run relevant checks, push, and monitor CI. Estimate: 20–30 implementation lines across both repositories, plus generated metadata and tests. Implementation: reuse the project ID already exported by the generated home-page module; no new metadata file or package. The generated Cloudflare Resource adapter passes the same ID to the SDK factory, which now requires it. Guards compare that ID before rendering or cache lookup. SDK and generated-adapter regression tests cover another project returning 200 and an unknown same-project alias returning 508. Hosted Worker tests verify the guard runs before the cache and permits another project's cached response. The original guard and a blanket-guard mutation fail the regressions. A local Cloudflare runtime reproduced the expected 200/508 behavior. No user-facing copy or documentation changes are needed; code comments state the cooperative boundary. The fix adds 19 implementation lines across both repos, below the 20–30 estimate, plus tests/configuration and regenerated fixtures. Existing deployments require republishing with the updated sender and guard to use project-specific markers. Final project-specific reentry verification: 930 SDK tests, 143 CLI prebuild tests, and 41 hosted-template tests passed. SDK/CLI types, published SDK types, lint, package boundaries, full fixture regeneration, both Cloudflare fixture builds, and the Pages function bundle passed. The actual Pages function returned 200 for another project's marker and 508 for its own marker through an unknown alias in a local Cloudflare runtime. All CI checks passed for Builder `433f9a359d469ca3669e383c4516c3e805ea22a1`, including all ten E2E shards and staging workflows; companion SaaS `3e28e7e33813dd968f33caac35dbbba52a3847d3` checks and preview deployment also passed. Agent evaluations were not run because they require separate approval; no evaluation changes were needed.
Content engine
The content engine compiles file-based Assets into a portable query database. JSON and Markdown documents can reference other Assets without embedding their complete source in that database.
Document references
A reference is an object with exactly one $ref property whose value is a URI
reference:
{
"author": { "$ref": "./authors/ada.json" }
}
References can appear at any depth in a JSON document. In Markdown, references can appear at any depth in YAML frontmatter:
---
title: Building with Markdown
author:
$ref: ./authors/ada.json
---
Article body.
An object with additional properties is ordinary content, not a reference.
The $ref spelling is familiar from JSON Schema, but this syntax does not
implement JSON Schema resolution, identifiers, anchors, or schema evaluation.
Reference paths use standard URI-reference resolution relative to the source document. The fragment selects which representation of the target to insert:
| Reference | Selected target value |
|---|---|
./author.json |
Complete JSON value |
./author.json#/profile/name |
Value at the JSON Pointer /profile/name |
./article.md |
Original Markdown source, including frontmatter |
./article.md#body |
Markdown body without frontmatter |
./article.md#frontmatter |
Markdown frontmatter as an object |
JSON Pointer escaping follows RFC 6901. For example, #/a~1b selects the
property named a/b. JSON Pointer fragments apply only to JSON documents;
#body and #frontmatter apply only to Markdown documents.
Authored URLs identify Assets during compilation. They do not permit arbitrary runtime network requests. Every target must match a document in the compiled Asset catalog.
Resolution behavior
At build time, the content engine:
- Parses bounded JSON sources and Markdown frontmatter.
- Resolves each static URI reference to a document identity and revision.
- Rejects missing targets, duplicate identities, revision conflicts, and dependency cycles.
- Stores the validated dependency graph with searchable metadata while source payloads remain in Asset storage.
At runtime, the server compares the query's filter, sort, and output property
paths with the graph's reference locations. If a reference can affect the
query, the server resolves the relevant source documents before executing the
query; otherwise it uses the normal query path without loading graph sources.
Non-reference filters first narrow the candidate sources. The server follows
only root references that intersect the query, preserves unselected sibling
markers, and computes the selected references' complete downstream dependency
closure. It loads independent documents concurrently, loads shared storage
content once, validates storage identities, revisions, formats, and payload
limits, and assembles dependencies before their consumers. A resolved value
replaces its exact { "$ref": "..." } marker.
Filtering, sorting, pagination, and projection therefore operate on resolved
values when their property paths intersect a reference. Only properties
selected by the Assets query are returned. Resolution cannot expose an
unselected property merely because it exists in a fetched document.
markdown-body-ref content selection is also graph-backed. Compilation retains the
Markdown document identity instead of its source bytes, and runtime hydration
fetches only the documents selected after filtering, sorting, and pagination.
The resolved query result continues to expose the body at content.text.
Whole-document and range content modes retain their embedded-content behavior.
Document source caches are keyed by storage reference, revision, and format, so content from one identity cannot be combined silently with different graph metadata. Published runtime assets must also point at the graph node's exact storage reference before the source is fetched. Resolution is bounded by document-count, per-document byte, aggregate-byte, and concurrency limits. A missing document, invalid representation, stale identity, limit violation, or cancellation fails the query instead of returning a partially assembled graph.
Assets without a document graph keep the existing embedded-content behavior and do not require migration.