Upload index.html with the assets, and keep error bodies that aren't JSON - #637
Merged
Merged
Conversation
ApiError.fromHttpError parsed the body as JSON and, when that threw, fell back to ky's own message — which names only the status and the URL. An opaque 403 or 502 therefore read identically whoever emitted it, and the server's own words were discarded before anyone saw them. This matters where it is hardest to reproduce: the sandbox publish path only ever surfaces this string, through the --json envelope, into a log. A run that failed with `Request failed with status code 403 Forbidden: POST .../finalize` gave no way to tell an app-level rejection from a proxy in front of the app, because the bytes that would have said so were already gone. Keep them instead, truncated so an HTML error page cannot flood the log, and when there are none report the content type — an empty body is itself a fingerprint of the layer that answered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YCz2yqUy1qj82YAsLXsoLt
A static deploy sent the built index.html as a file part on the finalize request. Cloudflare's WAF inspects that body, and a managed command-injection rule matches a backtick followed by a shell command with arguments — so an app whose markup contains, say, a documentation comment with `curl -s https://… ` was answered 403 at the edge, before the request reached us. No response body we could parse, no application log, and every publish of that commit failed the same way. Nothing about the app was wrong, and no retry could help: the bytes themselves were the trigger. Every other asset already goes straight to S3 through a presigned PUT, where nothing inspects it. The entry point now goes the same way, to a staging key the server copies into the build at finalize, so finalize carries no user-authored content at all. index.html stays the sentinel. It is still written last, by the server, only once every declared asset is present — the ordering that stops a partial build from reading as complete, and stops create short-circuiting a commit that never finished. A server that has not shipped this leaves `index_html_staged` unset, and the client keeps sending the bytes, so a CLI release need not be coordinated with a backend deploy in either direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YCz2yqUy1qj82YAsLXsoLt
Contributor
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.1.16-pr.637.845faaaPrefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.1.16-pr.637.845faaa"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.1.16-pr.637.845faaa"
}
}
Preview published to npm registry — try new features instantly! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Description
A static site deploy used to send the built
index.htmlas a file part on thefinalizerequest. Cloudflare's managed WAF inspects that body, and one app's markup contained a documentation comment with a backticked shell command — so every publish was answered403at the edge, 54/54 over a week, before the request ever reached the backend. This PR routes the entry point through the same presigned S3 PUT path as every other asset, sofinalizecarries no user-authored content at all, and separately makesApiError.fromHttpErrorkeep a non-JSON error body instead of discarding it — which is what made the original failure take a week to diagnose.Related Issue
Requires the server-side counterpart
base44-dev/apper#25992. Without it the server leavesindex_html_stagedunset and this client behaves exactly as before.Type of Change
Changes Made
feat(site): uploadindex.htmlwith the assets instead of at finalizeCreateDeploymentResponseSchemareads a newindex_html_stagedfield,.optional().default(false)— an older server leaves it unset and the client keeps sending the bytes, so the release needs no coordination with a backend deploy in either direction.FinalizePayloadbecomes an exported discriminated union (worker/static-inline/static-staged). The previous"indexHtml" in payloadshape could not express "send nothing", andRecord<string, never>types loosely inside a union.deployment.tssplits the oldreadIndexHtmlintorequireStaticEntryPoint(validation, still run beforecreateso a build that cannot be completed fails before any upload work) andresolveStaticCompletion(reads the bytes only when finalize will actually carry them).index.htmlitself goes up through the existinguploadPresignedAssetpath with no changes — the server signs it to a staging key and copies it into the build at finalize; the client does not need to know the key.fix(errors): keep an error response body that is not JSONApiError.fromHttpErrorpreviously fell back to ky's message, which names only the status and URL, so an opaque403or502read identically whoever emitted it.readResponseText(): reads aclone()so the caller's body stays unconsumed, never throws (it runs on a path already reporting a failure), and truncates at 500 chars so an HTML error page cannot flood a log.content-typeinstead — an empty body is itself a fingerprint of the layer that answered.Testing
npm test)New coverage:
static_site_deployments.spec.ts— a new "index.html staged through the presigned PUTs" suite asserting the entry point is PUT with the other assets (unsigned, byte-identical to the fixture) and thatfinalizeRequests[0]is[], i.e. no user HTML in a request body of ours. The existing legacy-path cases stay as backward-compat coverage.errors.spec.ts— three cases: an HTML body naming its emitter, an empty body reporting its content type, and truncation of a 2000-char body. One existing assertion changed:expect(apiError.responseBody).toBeUndefined()on a non-JSON body was pinning the exact defect.TestAPIServer.ts— the create-response type gains the optionalindex_html_stagedflag.Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
Why "all tests pass" is unchecked: the reported run is 843 passed with 2 failures in
dev.spec.ts, which reproduce identically on the untouched baseline (confirmed by stashing and re-running).bun run typecheckandbun run lintare clean.Reproduction (differential probe against production on a non-existent route, so the trigger is the body and not the path):
404from FastAPI — reached originindex.html403,server: cloudflare,<title>Blocked</title>Run `curl -s https://api.example.com/health` to check.curl -s https://x.com | grep(no backtick)No retry could help — the bytes were the trigger, and nothing about the app was wrong.
Not fixed here: the full-stack arm still POSTs worker modules through
finalize, and a JSDoc comment or help string containing a backticked command plus a URL in a.jsmodule part hits the same WAF rule (verified against production). A WAF exception scoped to the finalize path is still wanted — it is the only thing covering that arm, and it unblocks the affected app today rather than after a release plus sandbox-image pickup.🤖 Generated by Claude | 2026-09-23 04:58 UTC | 845faaa