FaceTheory Troubleshooting
Use this guide for recurring setup, build, and runtime failures that already have a verified diagnosis path.
Quick Diagnosis
| Symptom | Likely cause | Where to look |
|---|---|---|
npm ci or scripts fail early |
Node.js is below the required baseline | ts/package.json (engines.node: >=24) |
npm run ssg exits with usage errors |
Missing or invalid CLI flags | docs/api-reference.md and ts/src/ssg-cli.ts |
| SSG build fails during page generation | Network access was attempted without opting in | buildSsgSite() and SSG fetch guard behavior |
| SSG build fails with dot-segment output errors | generateStaticParams() returned . / .. path segments |
ts/src/ssg.ts path validation and route params |
| ISR route returns a deterministic 500 when tenant headers are present | Known tenant boundary headers reached ISR without an explicit tenant/cache partition | docs/migration-guide.md and ts/src/isr.ts tenant guard behavior |
| ISR object keys look duplicated | S3HtmlStore.keyPrefix and htmlPointerPrefix repeat the same prefix |
docs/core-patterns.md and docs/cdk/aws-deployment.md |
| React streaming misses late styles | styleStrategy: shell was used where all-ready was needed |
docs/core-patterns.md |
| Strict-CSP route fails before returning HTML | The route emitted inline hydration, inline styles/scripts, raw head, or unsafe body attrs | FaceRenderResult.csp and docs/core-patterns.md |
| Browser receives 404 or HTML for an SSR hydration sidecar | /_facetheory/ssr-data/... was not routed to the same FaceApp/Lambda handler as the SSR page |
ssrHydrationSidecars and Lambda URL routing |
| Form POST behind AppTheorySsrSite OAC returns 403 before Lambda | Native browser form cannot provide x-amz-content-sha256 for the Lambda URL OAC signing path |
startAwsOacFormTransport() and docs/core-patterns.md |
Issue: Node.js Version Mismatch
Symptoms:
npm ciwarns or fails on engines- local scripts fail before tests complete
Cause:
- FaceTheory requires Node.js
>=24.
Solution:
node --version
# switch to a supported version, then rerun:
cd ts
npm ci
npm run typecheck
npm test
Verification:
npm run typecheckpassesnpm testpasses
Issue: SSR Hydration Sidecar URL Returns 404 or HTML
Symptoms:
- SSR HTML contains
<link rel="facetheory-hydration" href="/_facetheory/ssr-data/..."> - the browser fetch for that exact URL returns
404 - or the fetch returns an HTML document instead of
application/json
Cause:
- FaceTheory SSR runtime sidecars use the reserved
/_facetheory/ssr-data/...prefix. - That prefix must reach the same Lambda/FaceApp handler that rendered the SSR HTML so the framework-owned resource route can read the stored render-time hydration payload.
- Do not route this prefix to S3/static SSG sidecar handling such as
/_facetheory/data/....
Solution:
- Keep SSR page requests and
/_facetheory/ssr-data/...requests on the same Lambda URL / FaceApp handling path. - Reserve S3/static handling for build-time SSG assets and sidecars.
Verification:
- a request for the emitted sidecar URL returns
content-type: application/json; charset=utf-8 - the response includes
cache-control: no-store - the response body is raw JSON and not a rendered HTML document
Issue: npm run ssg Fails With Argument Errors
Symptoms:
both --entry and --out are requiredinvalid value for --trailing-slashunknown argument: ...
Cause:
- The repository CLI wrapper only accepts the documented flag set.
Solution:
cd ts
npm run ssg -- --entry ./examples/ssg-basic/faces.ts --out ./tmp-ssg --trailing-slash always
Verification:
- The command prints
SSG complete: ... - Output files are written into
./tmp-ssg
Issue: SSG Build Fails Because fetch() Is Blocked
Symptoms:
- SSG route generation throws when page code tries to call the network
Cause:
buildSsgSite()disables real network access by default.
Solution:
- Prefer injecting a mocked
fetchin programmatic builds - Or allow real network access explicitly when that is intentional:
cd ts
npm run ssg -- --entry ./examples/ssg-basic/faces.ts --out ./tmp-ssg --allow-network
Verification:
- The build completes without the network guard error
Issue: SSG Build Fails With Dot-Segment Output Paths
Symptoms:
buildSsgSite()throws about prohibited dot-segments- a dynamic SSG route uses values such as
.or..
Cause:
generateStaticParams()returned path data that would collapse or escape the SSG output tree.
Solution:
- return only normal route segments from
generateStaticParams() - for catch-all routes, keep every segment inside the intended route subtree instead of using
./..
Verification:
- rerun the SSG build
- confirm the build succeeds and output files remain under the configured
outDir
Issue: ISR Fails Closed When Tenant Headers Are Present
Symptoms:
- an ISR route that previously rendered now returns a deterministic server error
- the request includes a tenant-like header such as
x-tenant-idorx-facetheory-tenant - the response does not include normal
x-facetheory-isr: miss|hit|wait-hit|staletransitions - expected ISR metadata or HTML objects are not written for that request
Cause:
- FaceTheory detected a tenant boundary signal on an ISR route that has neither an explicit
tenantKeynor a customcacheKey. The runtime fails closed before cache lookup, regeneration lease acquisition, metadata writes, or HTML writes so tenant-specific HTML cannot fall into the shared default cache partition.
Solution:
- If the route is actually tenant-invariant, strip viewer-supplied tenant-like headers at the CloudFront/AppTheory boundary before FaceTheory handles the request.
- If the route renders tenant-varying or personalized HTML, keep it as
mode: 'ssr'unless the HTML is safe to cache independently for each partition. - If tenant-varying ISR is intentional, configure
tenantKey: tenantKeyFromTrustedHeader('x-tenant-id')only after a trusted boundary strips viewer-supplied values and injects trusted tenant context, or provide a customcacheKeythat includes every request-varying dimension that changes the rendered HTML.
Verification:
- tenant-invariant ISR reaches normal
x-facetheory-isrstates once tenant-like headers are removed - an unpartitioned ISR request with tenant-like headers still fails closed and leaves no ISR cache writes behind
- explicitly partitioned tenant ISR returns separate cache entries for tenant A and tenant B
Issue: ISR HTML Keys Are Duplicated
Symptoms:
- S3 object keys look like
prefix/prefix/... - ISR misses expected cached HTML objects
Cause:
S3HtmlStore.keyPrefixandhtmlPointerPrefixwere configured with the same non-empty value.
Solution:
- Keep one prefix physical and the other logical, or leave one empty
- Re-run a known ISR route and inspect the written object keys
Verification:
- Repeated requests hit the same expected S3 object path
- Response headers show stable
x-facetheory-isrtransitions
Issue: Mutating Form POST Behind OAC Returns 403 Before Lambda
Symptoms:
- a same-origin HTML form POST through an AppTheorySsrSite CloudFront distribution returns
403 - the response mentions
InvalidSignatureExceptionor the request never reaches the AppTheory/FaceTheory Lambda logs - the affected route works when called by a tool that can set AWS signing payload headers
Cause:
- AppTheorySsrSite’s Lambda Function URL origin is protected by CloudFront Lambda URL OAC and
AWS_IAM. - Mutating requests such as
POST,PUT,PATCH, andDELETEneed thex-amz-content-sha256header so CloudFront can sign the exact payload bytes. - Native browser forms cannot add that header, and direct Lambda Function URL form actions bypass the supported CloudFront/AppTheory path.
Solution:
<form action="/agents/new" method="post" data-facetheory-oac-form>
<input name="agentName" required />
<button name="intent" value="create">Create agent</button>
</form>
import { startAwsOacFormTransport } from "@theory-cloud/facetheory";
startAwsOacFormTransport();
Keep the action same-origin. The helper computes the SHA256 digest over the exact URL-encoded bytes it sends, adds x-amz-content-sha256, and uses credentials: 'same-origin'.
Navigation behavior:
- the mutating fetch forces
redirect: "error", so HTTP redirects fail closed before a 307/308 can replay the URL-encoded body to another origin - server-rendered HTML validation/error responses may replace the whole document when they are not protected by response CSP headers
- HTML responses with
Content-Security-Policyheaders fail closed for fetched document replacement and explicit SPA DOM navigation because fetched response headers cannot become the active document policy - marked forms that resolve to
multipart/form-data,text/plain, or another unsupported encoding fail closed throughonErrorbefore sending - use
navigationPolicy: "full-page",onNavigate(context), oronResponse(response, context)when a host intentionally coordinates CSP-protected responses, post-submit redirects to safe GET URLs,startFaceNavigation(), or another caller-owned navigation layer
Verification:
- the request reaches the AppTheory/FaceTheory handler instead of failing at CloudFront/Lambda URL auth
- request headers include
x-amz-content-sha256andcontent-type: application/x-www-form-urlencoded;charset=UTF-8 - marked multipart or text/plain forms do not send a request and surface through
onError - redirects fail before body replay, and validation responses either stay same-origin with full-document outcomes or fail closed when response CSP cannot be preserved
Release handoff:
- validate the FaceTheory RC from its GitHub Release tarball in the consuming app rather than from a workspace link
- verify the same-origin action path is routed through AppTheory/CloudFront to Lambda, not S3 or a direct Function URL
- confirm Lambda receives the request with
AWS_IAM+ CloudFront OAC still enabled - confirm any app-local workaround or disabled-form path is removed only after the helper succeeds in the deployed CloudFront flow
- for the original theory-mcp-server lab driver, validate
POST /agents/newthrough CloudFront OAC before stable promotion - if emergency rollback is needed, pin the previous FaceTheory tarball or remove the opt-in marker/bootstrap; do not
keep
ssrUrlAuthType: NONEas a durable solution
Issue: Strict-CSP Route Fails Before Returning HTML
Symptoms:
- a route with
csp: { inlineScripts: false, inlineStyles: false, rawHead: false }returns a deterministic server error - the error mentions strict CSP rejecting inline script tags, inline style tags, raw head output, event handler attributes, style attributes, or non-external hydration
- a React streaming route errors when
styleStrategy: "shell"is used - a Svelte strict route errors after adding
<svelte:head>or component<style>output
Cause:
- Strict no-inline CSP is fail-closed. FaceTheory validates structured head output, adapter contributions, and the final rendered body before returning a strict document.
- Legacy
viteHydrationForEntry()emits inline__FACETHEORY_DATA__and is incompatible withinlineScripts:false. - React shell streaming can flush bytes before whole-document validation and is therefore rejected under strict
inlineScripts:false. - Adapter style extraction that emits
<style>tags, including Emotion/AntD inline extraction, is incompatible withinlineStyles:false.
Solution:
- Move hydration data to
externalHydrationForEntry(...)and serve the JSON from a same-origin URL. - Attach an explicit CSP header such as
buildStrictCspHeader()from the Face response. - Move CSS into the Vite client entry so
viteAssetsForEntry()emits external stylesheet links. - Replace raw head HTML and
<svelte:head>strict output with structured FaceTheoryheadTags. - For React streaming strict routes, use
styleStrategy: "all-ready"or buffer the route so validation completes before bytes flush.
Verification:
cd ts
npm run example:vite:svelte:strict-csp:build
node --import tsx test/unit/strict-csp-harness.test.ts
node --import tsx test/unit/vite-strict-csp-svelte-example.test.ts
If this is an AWS or release handoff, treat the local commands as runtime evidence only. They do not prove a release was published, a Simulacrum RC ran, or a CloudFront/S3/Lambda deployment is serving the route.
Issue: Streaming HTML Ships Without Expected Late Styles
Symptoms:
- HTML renders, but styles from async boundaries are missing from
<head>
Cause:
- React streaming was finalized at
shellinstead ofall-ready.
Solution:
renderOptions: {
styleStrategy: 'all-ready',
}
Verification:
- Re-run the affected route
- Confirm the expected style tags are present in server-rendered output
Getting Help
- Add newly repeated incidents here with a verified fix and a verification step.
- If a behavior is not formally documented yet, keep it out of the canonical troubleshooting guidance until it is confirmed.