Skip to content
Hoody.com

Hoody Pipe is a streaming data transfer service that brokers a direct connection between a sender and one or more receivers over HTTP. Senders POST or PUT data to a pipe path; receivers GET the same path and receive the bytes as they stream through, with original headers forwarded. No data is stored server-side — the server only holds the connection until both ends meet. Use these endpoints to move files, text, or arbitrary bytes between containers or between a container and an external service. The web UI and help endpoints sit alongside the streaming send/receive operations.

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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hoody Pipe</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<main id="app">Hoody Pipe — Streaming Data Transfer</main>
</body>
</html>

Content-Type: text/html

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.pipe.ui.getIndex();

Returns a pure HTML form for file or text upload that works without JavaScript. Useful in restricted browser environments or when JavaScript is disabled.

Query parameters control the form:

  • path pre-fills the pipe path and enables the send button.
  • mode switches between file (default) and text input.

The page uses a CSP nonce to block inline scripts. Path values are sanitized (leading slashes stripped, only URL-safe characters).

NameInTypeRequiredDescription
pathquerystringNoPre-fill the pipe path. Only URL-safe characters allowed.
modequerystringNoInput mode: file for file picker, text for textarea. Default: file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hoody Pipe — Send</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-abc123'; form-action 'self'">
</head>
<body>
<form action="/api/v1/pipe/mypath" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Send</button>
</form>
</body>
</html>

Content-Type: text/html

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.pipe.ui.getNoScript({ path: 'mypath', mode: 'file' });

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

Sections covered: receiving data, sending files, text, and directories with curl -T, the ?download and ?filename controls, ?video browser playback, ?progress transfer monitoring, and end-to-end encryption with OpenSSL.

Also accessible at /help.

This endpoint takes no parameters.

"Hoody Pipe 1.6.1\nStreaming Data Transfer over HTTP\n\n======= Get =======\ncurl https://pipe.example.com/mypath\n"

Content-Type: text/plain

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.pipe.info.getHelp();

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'.\n. Methods other than GET, HEAD, or OPTIONS return 405.

This endpoint takes no parameters.

{
"status": "ok",
"service": "pipe",
"built": "2024-01-15T10:00:00.000Z",
"started": "2024-01-20T08:30:00.000Z",
"memory": {
"rss": 25165824,
"heap": 8388608
},
"fds": 12,
"pid": 12345,
"ip": "10.0.0.5",
"userAgent": "hoody-pipe/1.6.1"
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.pipe.health.check();

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

Lifecycle:

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

Either side can connect first — the server holds the early party until the counterpart arrives (up to a 5-minute TTL).

Forwarded headers 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 digit string AND the body is non-multipart (multipart parts use chunked encoding).
  • Content-Disposition — if provided; a sender-supplied inline is upgraded to attachment unless the effective Content-Type is on the inline-safe allowlist.
  • X-Piping, X-Hoody-Pipe — custom metadata from sender.

Forwarded headers (Content-Disposition, X-Piping, X-Hoody-Pipe) are CRLF-sanitized to prevent header injection. The response always carries X-Robots-Tag: none and X-Content-Type-Options: nosniff.

Multi-receiver: When n > 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. The slowest receiver paces the entire transfer.

NameInTypeRequiredDescription
pathpathstringYesPipe path name to receive from. Must match the path used by the sender.
nqueryintegerNoExpected number of receivers. Must match the sender’s n value exactly — a mismatch returns 400. Default: 1.
downloadquerystringNoControl browser download behavior. Literal values: true, false, yes, no, 1, 0. Bare value ?download is also accepted.
filenamequerystringNoCustom download filename. Implies ?download — the response will have Content-Disposition: attachment; filename="<value>". Null bytes, CRLF, path separators, leading dots, and control characters are stripped. Truncated to 255 characters.
videoquerystringNoReturn an HTML page with an embedded MSE video player instead of raw pipe data. Only served to browsers (clients that send Accept: text/html). Literal values: true, false, yes, no, 1, 0.
progressquerystringNoReturn real-time transfer progress as SSE or an HTML dashboard. Does NOT consume a pipe receiver slot. Literal values: true, false, yes, no, 1, 0.

?download (bare), ?download=true, ?download=yes, ?download=1 force Content-Disposition: attachment. ?download=false|no|0 suppress Content-Disposition entirely. Absent means passthrough.

?filename overrides any filename from the sender’s Content-Disposition header.

?video shows an HTML player for WebM/MP4/MPEG-TS streams auto-detected from the first bytes.

?progress routes by Accept header: text/event-stream to SSE; text/html to a dashboard; otherwise SSE.

"<streamed bytes from sender>"

Headers always present:

  • X-Robots-Tag: none
  • X-Content-Type-Options: nosniff
  • Access-Control-Expose-Headers: X-Piping, X-Hoody-Pipe (only when the sender provided either custom header)

Forwarded headers from the sender (Content-Type, Content-Length, Content-Disposition, X-Piping, X-Hoody-Pipe) appear when present and not stripped.

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Simple receive
await client.pipe.pipe.receive('mypath');
// Two receivers with a custom filename
await client.pipe.pipe.receive('mypath', 2, 'true', 'report.txt');

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.

Lifecycle:

  1. Sender POSTs to a path — gets back a streaming response with [INFO] status messages.
  2. Server waits for n receivers to connect (default: 1).
  3. Once all receivers connect, data streams from sender to all receivers simultaneously.
  4. Sender receives [INFO] Upload complete. then [INFO] Transfer complete.

Status messages streamed to sender as text/plain:

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

Multipart uploads: When Content-Type is multipart/form-data, the server extracts the first file part (non-file form fields are drained and skipped) 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 and receivers receive 500.

Custom headers: Set X-Hoody-Pipe or X-Piping request headers to forward arbitrary metadata to receivers. Each header is capped at 8 KiB and CRLF/control chars are stripped.

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

NameInTypeRequiredDescription
pathpathstringYesUnique pipe path name. Must not be a reserved path (/, /help, /noscript, /favicon.ico, /robots.txt). Max 1024 characters.
nqueryintegerNoNumber of receivers to wait for before starting the transfer. All receivers get identical copies. Must be a positive integer, max 256. Default: 1.

The request body is optional and may be any content type. No body fields are defined in the schema — the raw bytes are streamed to receivers.

  • 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.
  • An empty POST is valid — receivers get an empty response.
"[INFO] Waiting for 1 receiver(s) to connect...\n[INFO] Streaming to 1 receiver(s)...\n[INFO] Upload complete.\n[INFO] Transfer complete.\n"

Other sequences the sender may see:

[INFO] A receiver disconnected.
[INFO] All receivers disconnected before transfer completed.
[ERROR] Timed out waiting for receivers.
[ERROR] Transfer failed — sender encountered an error.

The SDK call accepts only the path and an optional n. Body bytes are supplied at the HTTP layer:

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Open a send slot for one receiver
await client.pipe.pipe.send('mypath');
// Two receivers — same file goes to both
await client.pipe.pipe.send('mypath', 2);
Terminal window
# Send a file
curl -X POST -T report.pdf https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send stdin
echo 'hello world' | curl -X POST --data-binary @- https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send with custom metadata
curl -X POST -T report.pdf \
-H 'X-Hoody-Pipe: from=alice' \
-H 'X-Piping: encrypted=false' \
https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Wait for 2 receivers
curl -X POST -T report.pdf 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath?n=2'
// Send bytes via fetch
await fetch('https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath', {
method: 'POST',
body: fileBlob,
headers: {
'Content-Type': 'application/octet-stream',
'X-Hoody-Pipe': 'from=alice'
}
});

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.

All parameters, request body handling, status messages, and error codes are identical to POST.

Terminal window
# Send a file (curl -T defaults to PUT)
curl -T report.pdf https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send stdin as PUT
echo 'hello' | curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send a directory as tar.gz
tar czf - ./mydir | curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mydir.tar.gz
NameInTypeRequiredDescription
pathpathstringYesUnique pipe path name (same rules as POST — no reserved paths, max 1024 chars).
nqueryintegerNoNumber of receivers to wait for (must match receivers’ n, max 256). Default: 1.

The request body is optional and may be any content type. No body fields are defined in the schema — the raw bytes are streamed to receivers. Multipart/form-data is supported (first file part extracted; non-file fields skipped).

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

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.
  • 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.
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
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.pipe.pipe.corsPreflight('mypath');
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' \
-i \
https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath