Skip to content

feat(deploy): stream deploy_component as multipart/form-data - #530

Merged
kriszyp merged 6 commits into
mainfrom
feat/deploy-component-multipart
May 20, 2026
Merged

feat(deploy): stream deploy_component as multipart/form-data#530
kriszyp merged 6 commits into
mainfrom
feat/deploy-component-multipart

Conversation

@kriszyp

@kriszyp kriszyp commented May 14, 2026

Copy link
Copy Markdown
Member

Summary

  • Switches deploy_component from a CBOR-encoded Buffer payload to a chunked multipart/form-data body so components can exceed the Node.js 2 GB Buffer cap.
  • The CLI streams the tar+gzip directly from streamPackagedDirectory() into the request, never materializing the full archive in memory.
  • The operations API registers a multipart content-type parser that hands the route handler a Readable for the file part, which Application.extractApplication pipes straight into gunzip-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 for multipart/form-data. Calls done(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, done has 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.jsPREPARE_OPERATION.deploy_component no longer sets cborEncode; it builds a streamPackagedDirectory() and the dispatch code now constructs a multipart body for any _multipart request. TRANSPORT_ONLY_FIELDS is the new allowlist that keeps CLI-internal fields (target, credentials, etc.) out of the wire body.
  • utility/common_utils.jshttpRequest now .pipe()s a Readable body when given one; otherwise unchanged.
  • components/Application.tspayload?: Buffer | string | Readable now. The Readable branch is used directly; Buffer/base64 string branches are unchanged.
  • package-lock.json — adds busboy + @types/busboy. The lockfile also picks up some incidental cleanup that npm install did: @harperfast/integration-testing is upgraded to the version already specified in package.json's caret range, an unused node-unix-socket entry is removed, and some peer: true flags are reconciled. Worth a quick scan to confirm those are all benign.

Backward compatibility

  • The server still accepts application/json and application/cbor bodies for deploy_component (and every other operation), so external clients that built against the old wire format are unaffected.
  • packageDirectory() (buffered) is retained alongside the new streamPackagedDirectory() so any other caller depending on a Buffer return 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).
  • End-to-end deploy of a large component against a live Harper instance — not exercised locally; needs the integration test infra.

Follow-ups (not in this PR)

  • Improved deployment visibility #526: SSE-based progress reporting (phase/install/replicate events). The route handler will need to wire a progress emitter through deployComponent — the multipart parser already exposes the byte stream so upload-progress tracking can plug in at the parser layer too.
  • Support for deployments larger than 2GB #524 (replication slice): origin spools the multipart file part to a temp tarball on disk, mints a short-lived JWT via 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

@kriszyp
kriszyp requested a review from a team as a code owner May 14, 2026 03:19
@socket-security

socket-security Bot commented May 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​busboy@​1.5.41001007280100
Addedbusboy@​1.6.010010010080100

View full report

Comment thread package.json
@claude

claude Bot commented May 14, 2026

Copy link
Copy Markdown
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
kriszyp force-pushed the feat/deploy-component-multipart branch from dab9612 to b9780eb Compare May 14, 2026 04:07
Comment thread server/serverHelpers/multipartParser.ts Outdated
…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>

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing. much need!

Comment thread components/Application.ts
Kris Zyp and others added 4 commits May 19, 2026 07:47
…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>
@kriszyp
kriszyp merged commit 87da5a4 into main May 20, 2026
36 of 37 checks passed
@kriszyp
kriszyp deleted the feat/deploy-component-multipart branch May 20, 2026 04:36
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.

2 participants