feat(deploy): stream deploy_component as multipart/form-data - #530
Merged
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
|
Reviewed; no blockers found. |
Replaces the CBOR/Buffer payload path used by the CLI with a chunked multipart/form-data body so components larger than the Node.js 2 GB Buffer cap can be deployed end to end. The server-side parser exposes the file part as a Readable, and `Application.extractApplication` pipes it straight into gunzip + tar-fs.extract — no in-memory hop. This is the first slice of the >2 GB deploys + visibility work described in #524. Adds: busboy + @types/busboy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kriszyp
force-pushed
the
feat/deploy-component-multipart
branch
from
May 14, 2026 04:07
dab9612 to
b9780eb
Compare
6 tasks
…tegration test Addresses review feedback on #530: - multipartParser: a socket reset mid-upload (after the file part has started, i.e. after `done` has already fired) used to leave busboy and the file Readable open forever, so a downstream `pipeline(payload, gunzip(), extract(...))` would hang indefinitely. The rawStream error handler now also destroys body.payload directly — `bb.destroy()` does NOT propagate to the file Readable already handed out to the route handler. Added a regression test that reproduces the hang without the fix. - dependencies.md: documents busboy (size, security, alternatives, removal plan) per repo convention. - integrationTests/deploy/deploy-multipart-stream.test.ts: end-to-end test that POSTs a multipart deploy_component with a streamed package (including a multi-MB blob to cross several busboy chunk boundaries), verifies extraction succeeds, and verifies the deployed app is reachable. Complements deploy-from-source.test.ts which still covers the JSON/base64 path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kriszyp
pushed a commit
that referenced
this pull request
May 14, 2026
When the CLI sends `Accept: text/event-stream` on `deploy_component`, the operations API now returns Server-Sent Events instead of a single buffered response. A ProgressEmitter is attached to the operation request and the handler emits `phase` events at the extract → install → load → replicate → restart boundaries; the stream terminates with a `done` event (carrying the operation result) or an `error` event. The CLI parses the stream live, rendering each phase as it happens so multi-minute deploys no longer look hung. Non-SSE callers see no behavior change — the emitter is undefined on that path and every emission is optional-chained. Builds on #530. First slice of #526. Follow-ups: streaming live npm install stdout/stderr as `install` events, and re-emitting per-peer SSE events once the direct-HTTPS replication relay lands in #524 follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 14, 2026
…test The integration test was importing `streamPackagedDirectory` and `buildMultipartBody` via the `#src/*` import map. That works for unit tests run from the repo root but not from the integration test runner, which executes with `integrationTests/package.json` as its package context — that package only declares `"type": "module"` and doesn't inherit the root's import map. Switched to `../../dist/...` relative imports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflicts caused by the .js → .ts rename pass on main (most of the codebase moved from CommonJS to ESM TS in #460/#579). Specifically: - bin/cliOperations.ts: kept main's ESM imports + new CLI features (loadCredentials, isJWTExpired, normalizeTarget) and layered in the multipart streaming additions (streamPackagedDirectory, buildMultipartBody, TRANSPORT_ONLY_FIELDS, the _multipart branch, body=stream). - components/Application.ts: kept the Buffer | string | Readable payload switch from this branch (main's resolution was just a Buffer/base64 fallback). - server/serverHelpers/multipartParser.ts: updated the hdbError import from .js to .ts to match the rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s the html The 'deployed application reachable' assertion was failing because the fixture's config.yaml was missing the static block — without it Harper doesn't serve the web/ directory, so GET ctx.harper.httpURL got a non-200. Aligned the fixture's config with deploy-from-source/ fixture/config.yaml. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ethan-Arrowood
approved these changes
May 19, 2026
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.
Summary
deploy_componentfrom a CBOR-encodedBufferpayload to a chunkedmultipart/form-databody so components can exceed the Node.js 2 GB Buffer cap.streamPackagedDirectory()into the request, never materializing the full archive in memory.Readablefor the file part, whichApplication.extractApplicationpipes straight intogunzip-maybe+tar-fs.extract— no in-memory hop on the server either.Why
First slice of #524: support payload-based deploys larger than 2 GB. Next slices are SSE-based progress visibility (#526) and the per-peer HTTPS relay for replication (also in #524).
Where to look
server/serverHelpers/multipartParser.ts— busboy-driven parser registered as a Fastify content-type parser formultipart/form-data. Callsdone(body)as soon as the file part starts (not when it finishes) so the route handler can pump data through extraction with normal backpressure. Field-then-file ordering is a contract documented in the file header; out-of-order late fields are warned and ignored (see the inline comment for why we can't usefully propagate them — by the time we see them,donehas already fired and the file stream has typically ended).bin/multipartBuilder.ts— small CLI-side body builder, also used by the round-trip test. Fields are JSON-serialized on the wire when non-string so the server-side decoder can reverse it for booleans/numbers/objects.bin/cliOperations.js—PREPARE_OPERATION.deploy_componentno longer setscborEncode; it builds astreamPackagedDirectory()and the dispatch code now constructs a multipart body for any_multipartrequest.TRANSPORT_ONLY_FIELDSis the new allowlist that keeps CLI-internal fields (target, credentials, etc.) out of the wire body.utility/common_utils.js—httpRequestnow.pipe()s aReadablebody when given one; otherwise unchanged.components/Application.ts—payload?: Buffer | string | Readablenow. The Readable branch is used directly; Buffer/base64 string branches are unchanged.package-lock.json— addsbusboy+@types/busboy. The lockfile also picks up some incidental cleanup thatnpm installdid:@harperfast/integration-testingis upgraded to the version already specified in package.json's caret range, an unusednode-unix-socketentry is removed, and somepeer: trueflags are reconciled. Worth a quick scan to confirm those are all benign.Backward compatibility
application/jsonandapplication/cborbodies fordeploy_component(and every other operation), so external clients that built against the old wire format are unaffected.packageDirectory()(buffered) is retained alongside the newstreamPackagedDirectory()so any other caller depending on aBufferreturn value keeps working.Test plan
npx mocha unitTests/server/serverHelpers/multipartParser.test.js— 8/8 pass (field decoding, file part streaming, done-before-EOF, unexpected file field rejection, late field ignored, empty body, missing Content-Type, stream error propagation).npx mocha unitTests/bin/multipartBuilder.test.js— 6/6 pass (ordering, JSON-stringify of non-strings, undefined skipped, file-less form, unique boundaries, CR/LF + quote sanitization).npx mocha unitTests/components/packageComponent.test.js— 2/2 pass (full round-trip:streamPackagedDirectory→ multipart body → server parser → gunzip → tar extract; plus buffered/streaming size parity).npx mocha unitTests/server/fastifyRoutes/operations.test.js— 21/21 pass (existing deployComponent force-flag tests, unaffected).Follow-ups (not in this PR)
phase/install/replicateevents). The route handler will need to wire a progress emitter throughdeployComponent— the multipart parser already exposes the byte stream so upload-progress tracking can plug in at the parser layer too.create_authentication_tokens, and streams a direct HTTPS deploy to each peer with per-peer retry semantics. Out of scope here.🤖 Generated with Claude Code