Hoody Pipe
Section titled “Hoody Pipe”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.
Web Interface
Section titled “Web Interface”GET /api/v1/pipe
Section titled “GET /api/v1/pipe”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.
Response
Section titled “Response”<!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
SDK Usage
Section titled “SDK Usage”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();GET /api/v1/pipe/noscript
Section titled “GET /api/v1/pipe/noscript”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:
pathpre-fills the pipe path and enables the send button.modeswitches betweenfile(default) andtextinput.
The page uses a CSP nonce to block inline scripts. Path values are sanitized (leading slashes stripped, only URL-safe characters).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | query | string | No | Pre-fill the pipe path. Only URL-safe characters allowed. |
| mode | query | string | No | Input mode: file for file picker, text for textarea. Default: file. |
Response
Section titled “Response”<!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
SDK Usage
Section titled “SDK Usage”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' });Service Info
Section titled “Service Info”GET /api/v1/pipe/help
Section titled “GET /api/v1/pipe/help”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.
Response
Section titled “Response”"Hoody Pipe 1.6.1\nStreaming Data Transfer over HTTP\n\n======= Get =======\ncurl https://pipe.example.com/mypath\n"Content-Type: text/plain
SDK Usage
Section titled “SDK Usage”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();GET /api/v1/pipe/health
Section titled “GET /api/v1/pipe/health”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.
Response
Section titled “Response”{ "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"}SDK Usage
Section titled “SDK Usage”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();Data Transfer
Section titled “Data Transfer”GET /api/v1/pipe/{path}
Section titled “GET /api/v1/pipe/{path}”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:
- Receiver GETs a path — request blocks.
- When a sender POSTs/PUTs to the same path with a matching
n, the pipe establishes. - Response starts streaming the sender’s data.
- 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 totext/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-suppliedinlineis upgraded toattachmentunless 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.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | path | string | Yes | Pipe path name to receive from. Must match the path used by the sender. |
| n | query | integer | No | Expected number of receivers. Must match the sender’s n value exactly — a mismatch returns 400. Default: 1. |
| download | query | string | No | Control browser download behavior. Literal values: true, false, yes, no, 1, 0. Bare value ?download is also accepted. |
| filename | query | string | No | Custom 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. |
| video | query | string | No | Return 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. |
| progress | query | string | No | Return 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.
Response
Section titled “Response”"<streamed bytes from sender>"Headers always present:
X-Robots-Tag: noneX-Content-Type-Options: nosniffAccess-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.
"[ERROR] Receiver count mismatch — expected 2 but got 1.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_WORKER | Service Worker request blocked | Requests with Service-Worker: script header are rejected to prevent service worker registration via pipe paths. | Do not register service workers via pipe paths. |
ACTIVE_TRANSFER | Path has an active transfer | A transfer is already streaming on this path — no new receivers can join. | Wait for the transfer to complete, or use a different path. |
RECEIVER_SLOTS_FULL | All receiver slots taken | All n receiver slots for this path are occupied. | Wait for a receiver to disconnect, or use a different path. |
INVALID_N | Invalid receiver count | n is not a valid positive integer (1–256). | Set n between 1 and 256. |
N_MISMATCH | Receiver count mismatch | This receiver’s n does not match the existing sender/receivers on this path. | Use the same n value as the sender. |
"[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
METHOD_NOT_ALLOWED | HTTP method not supported | Pipe paths accept GET, POST, PUT, OPTIONS only. | Use GET to receive data. |
"[ERROR] Timed out waiting for sender.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
TTL_EXPIRED | Pipe TTL expired | Waited 5 minutes but counterpart did not connect. Pipe evicted. | Retry — ensure sender and receiver connect within 5 minutes. |
"[ERROR] Path too long (max 1024 characters).\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
PATH_TOO_LONG | Path exceeds length limit | Path exceeds 1024 characters. | Use a shorter path. |
"[ERROR] Too many pending transfers. Try again later.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
TOO_MANY_PENDING | Pending transfer limit reached | Server has 1000 pending pipes. | Wait and retry. |
TOO_MANY_ACTIVE | Active transfer limit reached | Server has 1000 active transfers — pipe established but cannot stream. | Wait for transfers to finish, then retry. |
SDK Usage
Section titled “SDK Usage”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 receiveawait client.pipe.pipe.receive('mypath');
// Two receivers with a custom filenameawait client.pipe.pipe.receive('mypath', 2, 'true', 'report.txt');POST /api/v1/pipe/{path}
Section titled “POST /api/v1/pipe/{path}”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:
- Sender POSTs to a path — gets back a streaming response with
[INFO]status messages. - Server waits for
nreceivers to connect (default: 1). - Once all receivers connect, data streams from sender to all receivers simultaneously.
- 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.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | path | string | Yes | Unique pipe path name. Must not be a reserved path (/, /help, /noscript, /favicon.ico, /robots.txt). Max 1024 characters. |
| n | query | integer | No | Number of receivers to wait for before starting the transfer. All receivers get identical copies. Must be a positive integer, max 256. Default: 1. |
Request Body
Section titled “Request Body”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-streamor the file’s actual MIME type. - Text: use
text/plain. - Multipart: use
multipart/form-datafor browser uploads — only the first file part is streamed. - An empty POST is valid — receivers get an empty response.
Response
Section titled “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."[ERROR] '/help' is a reserved path. Use a custom path like '/myfile' or '/transfer123'.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESERVED_PATH | Path is reserved | The requested path is a system-reserved path (/, /help, /noscript, etc.). | Choose a different path that is not reserved. |
DUPLICATE_SENDER | Path already has a sender | Another sender is already connected to this path waiting for receivers. | Use a different path, or wait for the existing transfer to complete. |
ACTIVE_TRANSFER | Path has an active transfer | The path is currently in use by a streaming transfer. | Wait for the current transfer to finish, or use a different path. |
INVALID_N | Invalid receiver count | The n query parameter is not a valid positive integer, or exceeds 256. | Set n to a positive integer between 1 and 256. |
N_MISMATCH | Receiver count mismatch | Sender’s n does not match existing receivers’ n on this path. | Use the same n value as the receivers. |
CONTENT_RANGE | Content-Range not supported | Content-Range headers are not supported for streaming transfers. | Send the complete file without range headers. |
"[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
METHOD_NOT_ALLOWED | HTTP method not supported | Pipe paths accept GET (receive), POST/PUT (send), and OPTIONS (CORS preflight). HEAD is supported on reserved paths only. | Use GET to receive data, POST or PUT to send data. |
"[ERROR] Path too long (max 1024 characters).\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
PATH_TOO_LONG | Path exceeds length limit | The pipe path exceeds the maximum length of 1024 characters. | Use a shorter path name. |
"[ERROR] Too many pending transfers. Try again later.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
TOO_MANY_PENDING | Pending transfer limit reached | Server has 1000 unestablished pipes. New transfers are rejected until existing ones complete or expire (5-minute TTL). | Wait for transfers to complete or expire, then retry. |
TOO_MANY_ACTIVE | Active transfer limit reached | Server has 1000 concurrent active transfers. | Wait for active transfers to finish, then retry. |
SDK Usage
Section titled “SDK Usage”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 receiverawait client.pipe.pipe.send('mypath');
// Two receivers — same file goes to bothawait client.pipe.pipe.send('mypath', 2);HTTP and cURL Examples
Section titled “HTTP and cURL Examples”# Send a filecurl -X POST -T report.pdf https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send stdinecho 'hello world' | curl -X POST --data-binary @- https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send with custom metadatacurl -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 receiverscurl -X POST -T report.pdf 'https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath?n=2'// Send bytes via fetchawait 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' }});PUT /api/v1/pipe/{path}
Section titled “PUT /api/v1/pipe/{path}”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.
# 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 PUTecho 'hello' | curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mypath
# Send a directory as tar.gztar czf - ./mydir | curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.com/api/v1/pipe/mydir.tar.gzParameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | path | string | Yes | Unique pipe path name (same rules as POST — no reserved paths, max 1024 chars). |
| n | query | integer | No | Number of receivers to wait for (must match receivers’ n, max 256). Default: 1. |
Request Body
Section titled “Request Body”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).
Response
Section titled “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""[ERROR] '/help' is a reserved path. Use a custom path like '/myfile' or '/transfer123'.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESERVED_PATH | Path is reserved | The requested path is a system-reserved path. | Choose a different path. |
DUPLICATE_SENDER | Path already has a sender | Another sender is already connected. | Use a different path or wait. |
INVALID_N | Invalid receiver count | n is not a valid positive integer (1–256). | Set n to a positive integer between 1 and 256. |
CONTENT_RANGE | Content-Range not supported | Content-Range headers are not supported. | Send the complete file. |
"[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
METHOD_NOT_ALLOWED | HTTP method not supported | Pipe paths accept GET, POST, PUT, OPTIONS only. | Use GET to receive, POST or PUT to send. |
"[ERROR] Path too long (max 1024 characters).\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
PATH_TOO_LONG | Path exceeds length limit | Path exceeds 1024 characters. | Use a shorter path. |
"[ERROR] Too many pending transfers. Try again later.\n"| Error Code | Title | Description | Resolution |
|---|---|---|---|
TOO_MANY_PENDING | Pending transfer limit reached | Server has 1000 pending pipes. | Wait and retry. |
OPTIONS /api/v1/pipe/{path}
Section titled “OPTIONS /api/v1/pipe/{path}”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-Methods—GET, POST, PUT, OPTIONS.Access-Control-Allow-Headers—Content-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-Pipe.Access-Control-Allow-Credentials—true(when Origin is present and notnull).Access-Control-Max-Age—86400(24 hours).Access-Control-Allow-Private-Network—true(when requested).
The "null" origin string is rejected — defaults to * which blocks credentialed requests from sandboxed iframes.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | path | string | Yes | Any path — OPTIONS is handled identically for all paths. |
Response
Section titled “Response”Access-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: GET, POST, PUT, OPTIONSAccess-Control-Allow-Headers: Content-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-PipeAccess-Control-Allow-Credentials: trueAccess-Control-Max-Age: 86400Access-Control-Allow-Private-Network: trueSDK Usage
Section titled “SDK Usage”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');HTTP and cURL Examples
Section titled “HTTP and cURL Examples”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