Skip to content

Hoody Pipe is a streaming data transfer service over HTTP. Any path on the Pipe API acts as a transient conduit between a sender and one or more receivers — the sender streams bytes in, the server forwards them to receivers as they connect, and the pipe evaporates when the transfer finishes. There is no intermediate storage and no buffering of the full payload.

Use the Pipe API to move files, text, or directories between containers and external services. The service also serves a browser UI for interactive transfers, exposes help text with copy-pasteable examples, and publishes a health endpoint.

The Pipe service serves two browser-based entry points. Both are unauthenticated and reachable from any origin.

Returns the Hoody Pipe web interface — an HTML page for sending files or text to a pipe path from the browser. Also accessible at / (root alias).

This endpoint takes no parameters.

Terminal window
curl https://pipe.example.com/api/v1/pipe

Response 200 — HTML page with the Hoody Pipe web interface.

<!doctype html>
<html lang="en">
<head><title>Hoody Pipe</title></head>
<body>... (Hoody Pipe web interface) ...</body>
</html>

Returns a pure HTML form for file or text upload that works without JavaScript. Useful in restricted browser environments. Also accessible at /noscript.

NameInTypeRequiredDescription
pathquerystringNoPre-fills the pipe path and enables the send button. Only URL-safe characters allowed.
modequerystringNoInput mode. Values: file (file picker) or text (textarea). Default: file.
Terminal window
# File picker with pre-filled path
curl "https://pipe.example.com/api/v1/pipe/noscript?path=myfile"
# Text input mode
curl "https://pipe.example.com/api/v1/pipe/noscript?path=notes&mode=text"

Response 200 — HTML form page with CSP nonce.

<!doctype html>
<html lang="en">
<head><title>Hoody Pipe — No Script Upload</title></head>
<body>
<form action="/api/v1/pipe/myfile" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Send</button>
</form>
</body>
</html>

Returns plain text usage instructions showing how to send and receive data using curl. The help text includes the server’s own URL (derived from the Host header) so examples can be copied and run directly.

This endpoint takes no parameters.

Terminal window
curl https://pipe.example.com/api/v1/pipe/help

Response 200 — Help text with ready-to-use curl examples.

Hoody Pipe 1.6.1
Streaming Data Transfer over HTTP
======= Get =======
curl https://pipe.example.com/mypath
======= Send =======
curl -T myfile https://pipe.example.com/mypath

Returns the standardized 9-field health response. Unauthenticated. Only reachable at /api/v1/pipe/health — a bare /health returns 404 with the body [ERROR] '/health' is not a valid path. Use '/api/v1/pipe/health'.. Methods other than GET, HEAD, or OPTIONS return 405 with [ERROR] Method &lt;verb&gt; is not allowed. and Allow: GET, HEAD, OPTIONS.

This endpoint takes no parameters.

Terminal window
curl https://pipe.example.com/api/v1/pipe/health

Response 200 — Service is healthy.

{
"status": "ok",
"service": "pipe",
"built": "2026-01-15T10:30:00.000Z",
"started": "2026-01-15T12:00:00.000Z",
"memory": {
"rss": 52428800,
"heap": 16777216
},
"fds": 12,
"pid": 12345,
"ip": "127.0.0.1",
"userAgent": "Hoody-Client/1.6.1"
}
FieldTypeRequiredDescription
statusstringYesAlways "ok" when healthy.
servicestringYesService identifier.
builtstringNoModule mtime as ISO 8601 string.
startedstringYesProcess start time as ISO 8601 string.
memoryobjectNoMemory usage — rss (resident set size in bytes) and heap (V8 heap used in bytes).
fdsintegerNoOpen file descriptor count.
pidintegerYesProcess ID.
ipstringYesServer IP address.
userAgentstringNoClient User-Agent header.

The data transfer endpoints share a common path scheme. A receiver issues GET /api/v1/pipe/{path} and blocks until a sender connects; the sender then issues POST or PUT to the same path with the same n value. Either side may connect first — the server holds the early party for up to 5 minutes.

Reserved paths (/, /help, /noscript, /favicon.ico, /robots.txt) return their own content rather than acting as pipe receivers.

NameInTypeRequiredDescription
pathpathstringYesPipe path name to receive from — must match the path used by the sender. Reserved paths (/help, /noscript, etc.) return their own content on GET instead of acting as pipe receivers.
nqueryintegerNoExpected number of receivers. Must match the sender’s n value exactly — a mismatch returns 400. When n &gt; 1, the pipe waits for all n receivers and the sender before streaming.
downloadquerystringNoControl whether the response triggers a browser download. - ?download (bare), ?download=true, ?download=yes, ?download=1 — force Content-Disposition: attachment (triggers download). Uses sender’s filename if available, otherwise pipe path basename. - ?download=false, ?download=no, ?download=0 — suppress Content-Disposition entirely, even if sender set one (forces inline display). - Absent — passthrough sender’s Content-Disposition as-is. Multipart form-data dispositions are auto-converted to attachment. Works per-receiver — with n=2, one receiver can have ?download and the other can display inline.
filenamequerystringNoSet a custom download filename. Implies ?download — the response will have Content-Disposition: attachment; filename="<value>". Priority: ?filename overrides any filename from the sender’s Content-Disposition header. Sanitization: Null bytes, CRLF, path separators (/, \), leading dots, and control characters are stripped. Truncated to 255 characters. Non-ASCII filenames use RFC 5987 filename*=UTF-8''... encoding. Filenames that sanitize to empty fall back to bare attachment.
videoquerystringNoReturn an HTML page with an embedded MSE (MediaSource Extensions) video player instead of raw pipe data. The player page fetches the raw stream internally — no pipe receiver slot is consumed by the page itself. Browser detection: Only serves the HTML player when the client sends Accept: text/html (i.e. a browser). Non-browser clients (VLC, mpv, curl, ffplay) with ?video fall through to normal pipe receiver behavior and get the raw stream — ensuring automatic compatibility with media players. Auto-detection: The player detects the container/codec from the stream’s first bytes: - WebM (VP8/VP9/AV1 + Opus/Vorbis) - MP4/fMP4 (H.264/H.265/VP09/AV01 + AAC) - MPEG-TS UI features: - Click to unmute (autoplay requires muted) - Right-click to pause/resume - Status overlay: “Waiting for stream…”, “Connected”, “Stream ended” - Buffer trimming (>30s behind currentTime removed) Values: ?video (bare), ?video=true, ?video=yes, ?video=1 → show player. ?video=false, ?video=no, ?video=0 → normal pipe receiver. Security: CSP with nonces (script-src, style-src), connect-src 'self', media-src blob:, default-src 'none'. Pipe path HTML-escaped in data-path attribute.
progressquerystringNoReturn real-time transfer progress as a Server-Sent Events (SSE) stream or HTML dashboard. Does NOT consume a pipe receiver slot — spectators are completely independent of the transfer. Accept header routing: - Accept: text/event-stream → SSE stream (EventSource, curl) - Accept: text/html → HTML dashboard page (browser) - Accept: */* or missing → SSE stream (default to data, not markup) SSE event types: - state — State transitions: idle → waiting → streaming → complete/failed - progress — During streaming (throttled 250ms): bytesTransferred, speed, ETA, receivers - done — Terminal event: final stats (bytesTransferred, duration, avgSpeed) State machine: idle (no pipe) → waiting (sender/receivers connecting) → streaming (data flowing) → complete or failed DoS protections: Max 50 spectators per path, 500 total groups, 30-min connection TTL, 30s post-transfer linger. Values: ?progress (bare), ?progress=true, ?progress=yes, ?progress=1 → show progress. ?progress=false, ?progress=no, ?progress=0 → normal pipe receiver. Security: HTML dashboard uses CSP with nonces. Pipe path HTML-escaped. SSE includes X-Accel-Buffering: no for Nginx compatibility.

Receive data from the specified pipe path. The response blocks until a sender connects and starts streaming. Once established, the response body contains the sender’s data with original headers forwarded.

Lifecycle:

  1. Receiver GETs a path → request blocks.
  2. When a sender POSTs/PUTs to the same path with matching n, the pipe establishes.
  3. Response starts streaming with the sender’s data.
  4. Response completes when sender finishes uploading.

Headers forwarded from sender:

  • Content-Type — sender’s content type (dangerous types rewritten to text/plain; foreign params dropped except a safe charset).
  • Content-Length — only when the sender provided a valid ^\d{1,19}$ value AND the body is non-multipart (multipart parts use chunked encoding).
  • Content-Disposition — if provided (e.g. attachment; filename="report.pdf"); a sender-supplied inline is upgraded to attachment unless the effective Content-Type is on the inline-safe allowlist (image/audio/video/text/plain/application/pdf, excluding image/svg+xml).
  • X-Piping — custom metadata from sender.
  • X-Hoody-Pipe — custom metadata from sender.

Forwarded headers are CRLF-sanitized (Content-Disposition, X-Piping, X-Hoody-Pipe) to prevent header injection.

Download control: Receivers can override Content-Disposition behavior using query parameters:

  • ?download — force attachment disposition (triggers browser download).
  • ?download=false — suppress Content-Disposition entirely (always display inline).
  • ?filename=custom.txt — set a custom download filename (implies ?download).
  • These work per-receiver — with n=2, one receiver can download while the other displays inline.

Multi-receiver: When n &gt; 1, all receivers get identical copies via lockstep fan-out — each chunk is written to every receiver before the next chunk is read from the sender. Memory is bounded to roughly one chunk per receiver, with no per-receiver queuing. Consequence: the slowest receiver paces the entire transfer — its backpressure applies to the sender and, transitively, to all other receivers. Fast receivers never run ahead of the slowest one.

Connection ordering: Receiver can connect before sender — server holds the connection until sender arrives (up to 5-min TTL).

Security headers on response:

  • X-Robots-Tag: none — prevents indexing.
  • X-Content-Type-Options: nosniff.
  • CORS headers reflecting the receiver’s Origin.

Also accessible without prefix (e.g. GET /myfile).

NameInTypeRequiredDescription
pathpathstringYesPipe path name to receive from — must match the path used by the sender. Reserved paths (/help, /noscript, etc.) return their own content on GET instead of acting as pipe receivers.
nqueryintegerNoExpected number of receivers. Must match the sender’s n value exactly — a mismatch returns 400. When n &gt; 1, the pipe waits for all n receivers and the sender before streaming. Default: 1.
downloadquerystringNoControl whether the response triggers a browser download. - ?download (bare), ?download=true, ?download=yes, ?download=1 — force Content-Disposition: attachment (triggers download). Uses sender’s filename if available, otherwise pipe path basename. - ?download=false, ?download=no, ?download=0 — suppress Content-Disposition entirely, even if sender set one (forces inline display). - Absent — passthrough sender’s Content-Disposition as-is. Multipart form-data dispositions are auto-converted to attachment. Works per-receiver — with n=2, one receiver can have ?download and the other can display inline.
filenamequerystringNoSet a custom download filename. Implies ?download — the response will have Content-Disposition: attachment; filename="&lt;value&gt;". Priority: ?filename overrides any filename from the sender’s Content-Disposition header. Sanitization: Null bytes, CRLF, path separators (/, \), leading dots, and control characters are stripped. Truncated to 255 characters. Non-ASCII filenames use RFC 5987 filename*=UTF-8''... encoding. Filenames that sanitize to empty fall back to bare attachment.
videoquerystringNoReturn an HTML page with an embedded MSE (MediaSource Extensions) video player instead of raw pipe data. The player page fetches the raw stream internally — no pipe receiver slot is consumed by the page itself. Browser detection: Only serves the HTML player when the client sends Accept: text/html (i.e. a browser). Non-browser clients (VLC, mpv, curl, ffplay) with ?video fall through to normal pipe receiver behavior and get the raw stream. Auto-detection: The player detects the container/codec from the stream’s first bytes — WebM (VP8/VP9/AV1 + Opus/Vorbis), MP4/fMP4 (H.264/H.265/VP09/AV01 + AAC), MPEG-TS. UI features: Click to unmute (autoplay requires muted); right-click to pause/resume; status overlay: “Waiting for stream…”, “Connected”, “Stream ended”; buffer trimming (>30s behind currentTime removed). Values: ?video (bare), ?video=true, ?video=yes, ?video=1 → show player. ?video=false, ?video=no, ?video=0 → normal pipe receiver. Security: CSP with nonces (script-src, style-src), connect-src 'self', media-src blob:, default-src 'none'. Pipe path HTML-escaped in data-path attribute.
progressquerystringNoReturn real-time transfer progress as a Server-Sent Events (SSE) stream or HTML dashboard. Does NOT consume a pipe receiver slot — spectators are completely independent of the transfer. Accept header routing: Accept: text/event-stream → SSE stream; Accept: text/html → HTML dashboard page; Accept: */* or missing → SSE stream. SSE event types: state (idle → waiting → streaming → complete/failed), progress (throttled 250ms: bytesTransferred, speed, ETA, receivers), done (terminal: bytesTransferred, duration, avgSpeed). DoS protections: Max 50 spectators per path, 500 total groups, 30-min connection TTL, 30s post-transfer linger. Values: ?progress (bare), ?progress=true, ?progress=yes, ?progress=1 → show progress. ?progress=false, ?progress=no, ?progress=0 → normal pipe receiver.
Terminal window
# Basic receive
curl https://pipe.example.com/api/v1/pipe/mypath -o downloaded.bin
# Download with custom filename
curl "https://pipe.example.com/api/v1/pipe/mypath?filename=report.bin" -o report.bin
# Inline display (suppress download)
curl "https://pipe.example.com/api/v1/pipe/mypath?download=false"
# Progress dashboard (browser)
curl -H "Accept: text/html" "https://pipe.example.com/api/v1/pipe/mypath?progress"
# Progress stream (curl/SSE)
curl -H "Accept: text/event-stream" -N "https://pipe.example.com/api/v1/pipe/mypath?progress"
# Multi-receiver
curl https://pipe.example.com/api/v1/pipe/mypath?n=2 -o copy.bin

Streamed data from sender. Content type matches what the sender sent (dangerous types rewritten to text/plain; foreign params dropped except a safe charset).

Forwarded headers (when present on the sender side and not stripped/capped): Content-Type, Content-Length (only when the sender sent a valid ^\d{1,19}$ value AND the body is non-multipart), Content-Disposition, X-Piping, X-Hoody-Pipe.

Always set on receiver response: X-Robots-Tag: none, X-Content-Type-Options: nosniff. CORS headers reflect the receiver’s request Origin.

HTTP/1.1 200 OK
X-Robots-Tag: none
X-Content-Type-Options: nosniff
Access-Control-Expose-Headers: X-Piping, X-Hoody-Pipe
Content-Type: application/octet-stream
<sender stream bytes>

Send data to the specified pipe path. The sender’s request body is streamed directly to receiver(s) when they connect — no server-side storage.

Status messages (streamed to the sender as text/plain):

[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.

Custom headers: Set X-Hoody-Pipe or X-Piping request headers to forward arbitrary metadata to receivers. Each header is capped at 8 KiB (over-cap headers are dropped) and CRLF/control characters are stripped. The receiver response carries Access-Control-Expose-Headers: X-Piping, X-Hoody-Pipe only when at least one was supplied.

Content-Type safety: Dangerous MIME types that execute scripts in browsers (text/html, image/svg+xml, application/javascript, the WHATWG JS-essence list, XHTML/XML) are rewritten to text/plain. Parameters are stripped except for a single safe charset (utf-8, us-ascii, iso-8859-1).

Limits: path length max 1024 characters; receiver count n 1–256; pending transfers max 1000 server-wide; active transfers max 1000 server-wide; unestablished pipe TTL 5 minutes.

Also accessible without prefix (e.g. POST /myfile).

NameInTypeRequiredDescription
pathpathstringYesUnique pipe path name. Must not be a reserved path (/, /help, /noscript, /favicon.ico, /robots.txt). Examples: myfile, transfer123, secret.png, logs/today.
nqueryintegerNoNumber of receivers to wait for before starting the transfer. All receivers get identical copies of the data (fan-out). Default: 1. Max: 256.

Data to stream to receiver(s). Any content type is accepted.

  • Binary files: Use application/octet-stream or the file’s actual MIME type.
  • Text: Use text/plain.
  • Multipart: Use multipart/form-data for browser uploads — only the first file part is streamed; leading non-file form fields are drained and skipped.
  • No body: An empty POST is valid — receivers get an empty response.

When Content-Type matches multipart/form-data (case-insensitive), the server extracts the first file part and streams its contents. The part’s Content-Type and Content-Disposition are forwarded to receivers. Content-Length is NOT forwarded for multipart inputs (the response uses chunked transfer encoding). A multipart body containing no file part is rejected with a [ERROR] Transfer failed status.

Terminal window
# Send a file (binary)
curl -T secret.png https://pipe.example.com/api/v1/pipe/secret.png
# Send text
curl --data-binary 'hello world' \
-H 'Content-Type: text/plain' \
https://pipe.example.com/api/v1/pipe/greeting
# Multipart file upload
curl -F 'file=@photo.jpg' https://pipe.example.com/api/v1/pipe/photo
# Multi-receiver fan-out
curl -T myfile https://pipe.example.com/api/v1/pipe/mypath?n=2
# Forward custom metadata
curl -T myfile -H 'X-Hoody-Pipe: job=build-42' https://pipe.example.com/api/v1/pipe/mypath

Sender connected. Response body streams [INFO]/[ERROR] status messages in real-time.

Success sequence:

[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.

Receiver disconnected:

[INFO] A receiver disconnected.
[INFO] All receivers disconnected before transfer completed.

Error:

[ERROR] Timed out waiting for receivers.
[ERROR] Transfer failed — sender encountered an error.

Identical to POST /api/v1/pipe/{path} — send data to the specified pipe path. PUT is provided as an alias because curl -T file URL uses PUT, making it natural for file transfers. The request body handling and status messages match POST.

The error codes for PUT differ slightly from POST: PUT does not emit ACTIVE_TRANSFER or N_MISMATCH on 400, and does not emit TOO_MANY_ACTIVE on 429 (only TOO_MANY_PENDING). See the response tables below for the exact list.

NameInTypeRequiredDescription
pathpathstringYesUnique pipe path name. Same rules as POST — no reserved paths, max 1024 chars.
nqueryintegerNoNumber of receivers to wait for. Default: 1. Must match receivers’ n. Max: 256.

Data to stream — any content type. multipart/form-data is supported (first file part extracted; non-file fields are skipped).

Terminal window
# Send a file (uses PUT)
curl -T myfile https://pipe.example.com/api/v1/pipe/mypath
# Send stdin
echo 'hello' | curl -T - https://pipe.example.com/api/v1/pipe/mypath
# Send a directory as tar.gz
tar czf - ./mydir | curl -T - https://pipe.example.com/api/v1/pipe/mydir.tar.gz
# Multi-receiver
curl -T myfile https://pipe.example.com/api/v1/pipe/mypath?n=2

Sender connected — streaming status messages (identical to POST).

[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.

Handles CORS preflight requests for cross-origin browser access. Returns permissive CORS headers reflecting the request Origin.

Headers returned:

  • Access-Control-Allow-Origin — reflects Origin (or * if none/null).
  • Access-Control-Allow-MethodsGET, POST, PUT, OPTIONS (HEAD is supported same-origin on reserved paths only).
  • Access-Control-Allow-HeadersContent-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-Pipe.
  • Access-Control-Allow-Credentialstrue (when Origin is present and not "null").
  • Access-Control-Max-Age86400 (24 hours).
  • Access-Control-Allow-Private-Networktrue (when requested).

The "null" origin string is rejected — defaults to * which blocks credentialed requests from sandboxed iframes.

NameInTypeRequiredDescription
pathpathstringYesAny path — OPTIONS is handled identically for all paths.
Terminal window
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, X-Hoody-Pipe" \
https://pipe.example.com/api/v1/pipe/mypath

Response 200 — CORS preflight response with permissive headers (empty body).

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-Pipe
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Access-Control-Allow-Private-Network: true