API Reference¶
All responses include standard headers: Server, Date, Connection, and
X-Content-Type-Options: nosniff (close by default, keep-alive when the
server keeps the socket open).
CORS headers are only emitted when CORS is explicitly enabled.
Error response bodies are endpoint-specific. Most shared handler and pipeline
errors use JSON format {"error": "message", "status": NNN}, but several
legacy paths return text, an empty body, or endpoint-specific JSON as documented
below.
Contract Stability¶
The surface described in this document is the legacy v0 API contract. The
server does not currently implement /api/v1 endpoints or a versioned API
prefix; clients should discover the active v0 method surface with PING.
The v0 compatibility promise is intentionally narrow:
- Documented method names, route shapes, filesystem scopes, and stable
PINGdiscovery fields should not be removed or renamed without a release note and migration path. PINGfieldssupported_methods,plugin_methods,access_scope, andmetricsare the stable discovery mechanism for clients. Treatsupported_methodsandplugin_methodsas sets. The additivemethod_groupsobject is presentation metadata and must not be treated as independent capability negotiation.- Response examples may gain additive JSON fields. Clients should ignore fields they do not understand unless this document marks the field as required.
- Legacy error bodies are not normalized across all endpoints. Status codes and documented stable fields are the contract; exact human-readable error text, JSON formatting, object key order, and mixed legacy text/empty bodies are not a v1-style error schema.
- Generated temporary names, generated request IDs, precise timing values, and
low-level counter names are operational diagnostics, not a stable client API.
They may change as long as
PINGdiscovery remains available. - Advanced upload fallback,
SMUGGLE,NOTE, and WebSocket notes are legacy v0 behavior in the single full mode; they are not a promise that the same shape will become the future v1 API.
The built-in browser UI, bundled examples, and operator-owned scripts are reference consumers of this legacy surface, not an official SDK or a public client support program with broader compatibility guarantees.
There is no global idempotency key in v0. Read-only methods (GET, HEAD,
FETCH, INFO, PING, and OPTIONS) are the safest retry targets. Mutating
methods can create, replace, delete, or clear state before a client observes a
timeout. Retrying a mutating request should use a stable filename or note ID
when the endpoint supports one, then confirm final state with INFO, FETCH,
NOTE /notes, or NOTE /notes/{id}. X-Request-Id is generated by the server
for log correlation after dispatch, not client deduplication.
NOTE IDs are legacy v0 identifiers. Server-generated note IDs are 32 lowercase
hex characters. Current v0 validation accepts client-supplied lowercase hex
IDs from 1 to 32 characters for existing-note paths and idempotent saves, but
clients should send 32-character IDs for forward compatibility. For idempotent
NOTE creates, send a stable id (HTTP) or noteId (WebSocket) with
createIfMissing: true. WebSocket opId is only echoed for acknowledgement
correlation; it is not stored, replayed, or used to deduplicate saves.
Transport-layer failures are distinct from handler responses. Some receive-layer framing failures close the TCP connection before an HTTP response exists, and WebSocket protocol failures after a successful upgrade use WebSocket close frames instead of HTTP JSON bodies. The response-body tables below apply only when the server emits an HTTP response or an application-level WebSocket JSON frame.
ADR-010 defers /api/v1 and official SDK/public-client work until explicit
versioning, error/idempotency, feature-selection, security-boundary, and
ownership decisions are approved. If v1 work ever starts, it should be
explicit and opt-in, such as an /api/v1/... route family or another
versioned negotiation mechanism. A small v1 surface would likely define
normalized JSON errors, explicit idempotency, versioned discovery, ordinary
upload/list/delete operations, NOTE CRUD, and a versioned notes WebSocket
contract. None of that v1 surface exists today.
Error Response Bodies¶
| Surface | Error body contract |
|---|---|
| GET | 404 uses JSON {"error": "File not found: <path>", "status": 404}. |
| HEAD | Same status and headers as GET, but with an empty body. |
| POST / PUT / PATCH / NONE | Empty uploads return JSON {"success": false, "error": "...", "hint": "..."}. Write failures return JSON {"success": false, "error": "..."}. Upload-size failures that reach the handler/pipeline use JSON {"error": "...", "status": 413}; receive-layer oversized request guards can close before dispatch. |
| DELETE | File/path validation errors generally use JSON {"error": "...", "status": NNN}. Clear-upload failures use endpoint JSON with success, error, deletion counters, preserved, and errors. |
| FETCH | Missing files return legacy text/plain body Cannot fetch: <path> with X-Fetch-Status: file-not-found. |
| INFO | Invalid paths return legacy text/plain body Invalid path. Missing paths return JSON {"exists": false, "path": "<path>"}. Hidden paths use the shared JSON error body. |
| SMUGGLE | Missing files, builder validation, source-size, and temp-retention failures return JSON with human error, numeric status, and a stable machine-readable code; field-specific validation may also include field. Existing legacy keys such as path, size fields, and error are preserved for backward compatibility. |
| NOTE HTTP | Validation, missing-note, and crypto-unavailable errors use JSON {"error": "...", "status": NNN}. NOTE /notes/key reports crypto availability in its normal 200 response. |
| WebSocket upgrade and messages | Auth failures can return 401/429 JSON before WebSocket validation. HTTP upgrade rejections (400, 403, 501, 503) use JSON {"error": "...", "status": NNN} before the WebSocket handshake. After upgrade, application-message errors are WebSocket JSON text frames such as {"type": "error", "error": "..."} or operation frames that may include error and status; protocol/frame failures close with WebSocket close frames instead. |
| Advanced upload | Unknown methods carrying an advanced payload in a supported body format, headers, query string, cookies, or an explicitly marked path segment are routed to advanced upload. Unknown methods without an advanced payload return shared JSON 405. Some validation errors use JSON {"error": "...", "status": 400}. Missing advanced payloads after dispatch return 400 application/json containing status: 400 and the standard always-on upload diagnostic fields; the body is not empty text. HMAC failures return JSON {"ok": false, "err": "hmac"}. Write failures return JSON {"ok": false}. |
| Advanced routing control | Malformed or incomplete PUT JSON, malformed diagnostics-only PATCH JSON, and invalid prefix/decoder values return shared JSON 400. A plugin conflict returns JSON 409 only for PUT. Unauthorized control access returns JSON 403. Other methods return JSON 405 with Allow: GET, PUT, PATCH, DELETE. |
| Auth and request guards | Basic-auth failures, auth rate limits, internal pipeline errors, and aggregate body-memory budget exhaustion use JSON {"error": "...", "status": NNN}. Other receive-layer framing failures such as unsupported Transfer-Encoding, conflicting or invalid Content-Length, declared Content-Length over the configured upload cap, receive timeouts, or requests that exceed the receive hard cap may close the connection without an HTTP error body. |
Full Method Surface¶
XFerry has one always-on core method surface. The handler registry, exact-origin CORS preflight, browser UI affordances, and WebSocket notes all use this full surface by default. Core methods are:
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, FETCH, INFO,
PING, NONE, NOTE, SMUGGLE, plus unknown non-standard methods carrying
an advanced-upload payload.
Wildcard CORS remains read-only and lists only read methods; exact CORS origins can receive the full method list and can echo a requested unknown advanced upload method when the method token is valid.
Request Framing and Caps¶
The receive layer enforces protocol framing before handler dispatch:
- Request headers are capped by
--max-header-size KB(64KiB by default) before the terminating blank line. - Request bodies are capped by
--max-size MB(100MiB by default) using the declaredContent-Lengthand the bytes actually read. - Concurrent in-flight request bodies are reserved against
--body-memory-budget MB. The default budget is--workers * --max-size. Aggregate budget exhaustion returns503before the remaining body bytes are read. - Active WebSocket upgrades are capped by
--max-websocket-connections N. The default is--workers // 2;0rejects all WebSocket admissions with503. - Incomplete WebSocket frames are capped by
--websocket-frame-idle-timeout SECONDS(5seconds by default). Timeout failures close the WebSocket with protocol close code1002. - Aggregate disk usage under
uploads/is controlled separately with optional--upload-storage-limit MB,--upload-file-limit N, and--upload-reserve-free MBlimits. A value of0disables each aggregate limit. - Encrypted Notepad blobs under
notes/are capped by--note-storage-limit MBand--note-count-limit N(256MiB and1000notes by default). A value of0disables each aggregate Notepad limit. - Generated one-shot SMUGGLE pages are retained under
uploads/only within--smuggle-temp-age SECONDS,--smuggle-temp-file-limit N, and--smuggle-temp-storage-limit MB(3600seconds,32files, and128MiB by default). A value of0disables the corresponding retention limit. Transfer-Encodingis unsupported and rejected at the receive layer because the server does not decode chunked request bodies.- Invalid, negative, or conflicting duplicate
Content-Lengthvalues are rejected. Duplicate identicalContent-Lengthvalues are accepted. - Receive-layer framing failures may close the connection before an HTTP error
response is built, except aggregate body-memory budget exhaustion, which
returns a JSON
503. Rejections are counted inmetrics.receive_rejectionsand summarized undermetrics.receive.
GET¶
Serve the bundled web UI, bundled static assets, and user files from uploads/.
Request:
GET / and GET /index.html serve the built-in UI. GET /static/... serves
the built-in UI assets. Other file paths are resolved inside uploads/;
/file.txt and /uploads/file.txt both target <root>/uploads/file.txt.
Response: File contents with appropriate Content-Type. Bundled HTML files
include Content-Security-Policy; uploaded HTML/SVG files are forced to
download as attachments.
The bundled UI CSP currently includes default-src 'self', script-src
'self', style-src 'self' 'unsafe-inline', img-src 'self' data:,
connect-src 'self' ws: wss:, base-uri 'self', object-src 'none',
frame-ancestors 'none', and form-action 'self'. Inline scripts are blocked.
The remaining inline style allowance is limited to current UI progress widgets.
Status codes: 200 OK, 304 Not Modified (if ETag matches), 404 Not Found
HEAD¶
Returns the same headers as GET but with no response body. Useful for checking file existence and metadata without transferring content.
Request:
Response: Same status code and headers as GET (200 or 404), empty body.
Status codes: 200 OK, 304 Not Modified (if ETag matches), 404 Not Found
Basic Upload: POST / PUT / PATCH / NONE¶
All four methods use the same Basic handler unless active Advanced prefix routing matches the request path. Basic has three exact wire profiles:
| Profile | Request target | Body/headers | Filename source |
|---|---|---|---|
| Multipart (default) | /uploads |
The browser UI sends one FormData file part using field file; the server accepts any non-empty file-part field name. The browser owns the multipart boundary and Content-Length. |
X-File-Name, then part filename, then URL, then generated |
| Raw URL | /uploads/<encoded-name> |
Original file bytes, no X-File-Name |
URL |
| Raw Header | /uploads |
Original bytes, Content-Type: application/octet-stream, URL-encoded X-File-Name |
header |
Example Raw Header request:
POST /uploads HTTP/1.1
Content-Type: application/octet-stream
X-File-Name: myfile.txt
Content-Length: 1234
<file bytes>
Filename precedence is X-File-Name > multipart file-part filename > URL
path > generated timestamp name. The /uploads collection special case
applies only to multipart: a raw request to /uploads without
X-File-Name saves a literal filename uploads. X-File-Name values are
URL-decoded and sanitized before publication. A multipart part filename is
parsed and sanitized but is not URL-decoded by XFerry. Collisions receive a
safe suffix.
For Basic multipart, scalar form fields are ignored. The request must contain
exactly one top-level file part with a non-empty payload. Zero or multiple file
parts, an empty file payload, nested multipart parts, malformed boundaries or
part headers, and duplicate singleton MIME part headers (Content-Disposition,
Content-Type, or Content-Transfer-Encoding) are rejected with 400.
Content-Transfer-Encoding may be absent or use binary / 8bit
case-insensitively. Unsupported encodings are rejected and are never decoded.
Response (201):
{
"success": true,
"filename": "myfile.txt",
"size": 1234,
"size_human": "1.2 KB",
"path": "/uploads/myfile.txt",
"uploaded_at": "2025-01-15T10:30:00",
"content_type": "text/plain"
}
Every Basic response, including errors, includes additive JSON diagnostics:
dispatch, route_source, route_revision, profile, carrier,
filename_source, normalized_filename, collision_renamed,
request_body_size, payload_size, file_content_type, and sha256.
The digest is SHA-256 over the final payload bytes, not the multipart envelope.
The browser comparison uses that digest plus strict diagnostics for the three
profiles. Optional response mirrors may be disabled; verdicts report observed
equivalence/difference and never claim that an SWG caused the result.
Headers: X-Upload-Status, X-File-Name, X-File-Size, X-File-Path.
The six diagnostic mirror headers documented below are optional and default
off.
Status codes: 201 Created, 400 No data, 413 Payload too large, 500 Server error
DELETE¶
Delete a file from uploads/. Only files inside uploads/ can be deleted.
To clear the upload workspace, use the explicit clear flag; plain
DELETE /uploads still rejects directory deletion.
Request:
Response (200):
Clear uploads request:
Clear uploads response (200):
{
"success": true,
"cleared": true,
"path": "/uploads",
"deleted_files": 3,
"deleted_dirs": 1,
"preserved": [".gitkeep"]
}
Hidden service files such as .gitkeep are preserved. Current notepad storage
lives in the separate top-level notes/ directory; uploads/notes/ is treated
as ordinary upload content.
Status codes: 200 OK, 403 Outside uploads/, 404 Not Found, 400 Cannot delete directory
FETCH¶
Download a file with Content-Disposition: attachment.
Request:
Response: File contents with download headers.
Headers: Content-Disposition, X-Fetch-Status, X-File-Name, X-File-Size, X-File-Modified
Status codes: 200 OK, 404 Not Found
INFO¶
Directory listing as JSON. Supports pagination via query parameters. Paths are
always resolved inside uploads/; / and /uploads/ both describe the upload
workspace.
Request:
Query parameters:
- offset (default: 0) — Skip first N items
- limit (default: 100, max: 1000) — Number of items to return
- inspect=1 — Opt in to bounded content inspection metadata. Any omitted or
other inspect value preserves the legacy response shape and does no content
inspection work.
Response (200):
{
"exists": true,
"path": "/uploads/",
"name": "uploads",
"is_file": false,
"is_directory": true,
"size": 4096,
"size_human": "4.0 KB",
"content_type": "unknown",
"created": "2025-01-15T10:30:00",
"modified": "2025-01-15T10:30:00",
"extension": "",
"access_scope": "uploads",
"total_items": 42,
"offset": 0,
"limit": 100,
"contents": [
{
"name": "file.txt",
"is_dir": false
}
]
}
Directory contents entries include only name and is_dir by default;
request INFO for a specific child path to retrieve size, timestamps, content
type, and other file metadata for that entry.
Optional content inspection¶
Request inspection explicitly, for example:
The response then gains this additive inspection object for an individual
file (or for eligible file entries in a directory listing):
{
"inspection": {
"mime_type": "application/pdf",
"mime_source": "signature",
"content_state": "recognized",
"warning": null,
"reasons": []
}
}
mime_source is one of signature, text, extension, or unknown.
content_state is recognized, opaque, or unknown. warning is null,
possible_encrypted_or_packed, or extension_mismatch. reasons may contain
encrypted_suffix, extension_mismatch, unrecognized_binary,
insufficient_data, or unavailable.
Inspection is heuristic metadata, not a security verdict: no numeric
probability or entropy score is reported, and an opaque file never proves XOR
encryption. A .enc or .xor suffix is only a reason. Password-protected
ZIP-family or other container files are identified only by their outer format,
not as proof of their contents or encryption.
The server reads at most 65,536 bytes from the file head. For ZIP-family, PE/SFX, or otherwise opaque candidates it may use a separately bounded, 65,557-byte ZIP-tail sample solely to confirm the outer ZIP format. For a directory request, it sorts and paginates first, then inspects only regular, non-symlink files in the visible page; directories and entries outside that page are not inspected.
Status codes: 200 OK, 400 Invalid path, 404 Not Found
PING¶
Health check endpoint.
Request:
Response (200):
{
"status": "pong",
"server": "XFerry/2.1.0",
"timestamp": "2025-01-15T10:30:00.123456+00:00",
"supported_methods": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "FETCH", "INFO", "PING", "NONE", "NOTE", "SMUGGLE"],
"method_groups": {
"request": ["GET", "HEAD", "OPTIONS", "INFO", "PING"],
"upload": ["POST", "PUT", "PATCH", "NONE"],
"files": ["DELETE", "FETCH", "SMUGGLE"],
"notepad": ["NOTE"]
},
"plugin_methods": [],
"access_scope": "uploads",
"metrics": {
"uptime_seconds": 3600.5,
"total_requests": 150,
"total_errors": 2,
"client_errors": 4,
"server_errors": 2,
"bytes_sent": 524288,
"bytes_received": 1048576,
"status_counts": {
"200": 144,
"404": 4,
"500": 2
},
"receive_rejections": {
"header_too_large": 1,
"body_too_large": 1
},
"connections": {
"active": 3,
"accepted": 150,
"closed": 147
},
"receive": {
"bytes": 1048576,
"rejections": 2,
"rejection_reasons": {
"header_too_large": 1,
"body_too_large": 1
}
},
"timeouts": {
"websocket_incomplete_frame": 1
},
"request_admission": {
"active": 2,
"accepted": 150,
"rejected": 1
},
"body_memory": {
"max_bytes": 1048576000,
"current_bytes": 0,
"peak_bytes": 52428800,
"rejected": 0
},
"request_latency_ms": {
"count": 150,
"total": 2450.75,
"avg": 16.338,
"max": 180.25
},
"websocket": {
"active": 0,
"rejected_admissions": 1,
"closed": 4,
"protocol_errors": 0,
"message_too_big": 0,
"incomplete_frame_timeouts": 1,
"idle_pings": 3,
"errors": 0
},
"worker": {
"exceptions": 0,
"exception_sources": {},
"last_exception_type": null
},
"storage": {
"usage": {
"notes": {"bytes": 4096, "items": 2, "notes": 2},
"smuggle_temp": {"bytes": 8192, "items": 1, "files": 1},
"uploads": {"bytes": 532480, "items": 6, "files": 6}
},
"quota_denials": {
"notes": {"bytes": 0, "notes": 0},
"smuggle_temp": {"bytes": 0, "files": 0},
"uploads": {
"bytes": 1,
"disk_full": 0,
"files": 0,
"free_space": 0
}
},
"scans": {
"info": {
"count": 3,
"items": 18,
"total_ms": 1.2,
"avg_ms": 0.4,
"max_ms": 0.6
},
"notepad_listing": {
"count": 1,
"items": 2,
"total_ms": 0.3,
"avg_ms": 0.3,
"max_ms": 0.3
},
"notepad_usage": {
"count": 2,
"items": 3,
"total_ms": 0.4,
"avg_ms": 0.2,
"max_ms": 0.25
},
"storage_snapshot": {
"count": 3,
"items": 9,
"total_ms": 0.9,
"avg_ms": 0.3,
"max_ms": 0.5
},
"upload_quota": {
"count": 4,
"items": 20,
"total_ms": 2.4,
"avg_ms": 0.6,
"max_ms": 0.8
}
}
},
"advanced_upload": {
"decode_rejections": {
"crypto_unavailable": 0,
"decoded_too_large": 1,
"decrypt_failed": 0,
"encoded_too_large": 0,
"hmac_mismatch": 0,
"invalid_encoding": 1,
"invalid_key": 0
}
}
}
}
supported_methods is the availability source of truth for built-in core
methods. method_groups is derived presentation metadata for organizing those
methods in clients; it does not enable or disable a feature independently.
Clients should treat supported_methods and plugin_methods as sets and
ignore unknown additive fields. Plugin methods remain separate and are not
inserted into core groups. Legacy discovery fields profile, capabilities,
and advanced_upload are no longer emitted.
When the core SMUGGLE implementation is active, PING also includes additive
smuggle_capabilities for UI clients. The object is advisory discovery data,
not an authorization decision. It includes source_max_bytes, field_limits,
defaults, extensions, mime_presets, mime_by_extension, presets,
locales, constructor enum lists, trigger_events, trigger_aliases,
custom_trigger_methods, temp_policy, and boolean caps. Current defaults,
limits, and built-ins are:
- default simple mode:
preset=direct; default constructor mode:locale=ru,encrypt=false,payload_encoding=b64,trigger_method=svg,trigger_event=onload,output_format=html,download_variant=blob-anchor,page_template=default,mime_type=application/octet-stream,null_byte=false,show_notice=true,use_constructor=false - field limits:
download_name120 characters,download_ext32 characters,title120 characters,message280 characters,cta_label80 characters,delay_ms0..10000,mime_type120 characters, andtrigger_event64 characters - locales:
ru,en - suggested extracted-file extensions:
txt,bin,dat,zip,pdf;extensionsis a UI suggestion list, not an allowlist or a content-safety boundary - constructor MIME presets cover generic/text (
application/octet-stream,text/plain,text/html,text/css,text/csv,text/javascript,application/json,application/xml,application/pdf), archives (application/zip,application/gzip,application/x-tar,application/x-7z-compressed,application/vnd.rar), images/media (image/png,image/jpeg,image/gif,image/webp,image/svg+xml,audio/mpeg,video/mp4), legacy and OOXML Office types (application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation), packages/binaries (application/java-archive,application/vnd.android.package-archive,application/wasm,application/vnd.microsoft.portable-executable,application/x-msi), and scripts (text/x-python,application/x-powershell,application/x-sh) mime_by_extensionsupplies matching suggestions forbin,dat,txt,log,md,csv,html,htm,css,js,mjs,json,xml,pdf,zip,gz,tgz, compoundtar.gz,tar,7z,rar,png,jpg,jpeg,gif,webp,svg,mp3,mp4,doc,docx,xls,xlsx,ppt,pptx,jar,apk,wasm,exe,dll,scr,msi,py,pyw,ps1,psm1,psd1,sh,bash, andzsh- simple presets:
direct,card_manual,card_auto - payload encodings:
b64,base64url,base32,percent,reverse,xor,hex,split,attrs,charcode - outer artifact formats:
html,htm,shtml,shtm,xhtml,xht,xhtm,xml,svg(this expansion adds no output formats) - page templates:
default,minimal,corporate,drive,npf-zip-archive-help,npf-rar-archive-help - download variants:
blob-anchor,data-uri,iframe-blob,filereader,fetch-blob,window-open,loc-assign,form-post,timeout-blob,promise-blob,raf-blob,microtask-blob,observer-blob,response-blob,readable-stream,message-channel-blob,idle-callback-blob - trigger map:
svg:onload;body:onload,onpageshow;img:onerror,onload;audio:onerror,onloadstart;video:onerror,onloadstart;source:onerror;input:onfocus,oninput,onchange,onkeydown;select:onfocus,onchange;button:onfocus,onclick,onpointerdown,onkeydown;textarea:onfocus,oninput,onchange,onkeydown;details:ontoggle,onclick;iframe:srcdoc,onload;animate:onbegin,onend,onrepeat;animmotion:onbegin,onend,onrepeat;set:onbegin,onend;cssanim:onanimationstart,onanimationend,onanimationiteration;csstransition:ontransitionrun,ontransitionstart,ontransitionend;link:onerror,onload;script:onerror;form:onsubmit;custom:onfocus;focusin:onfocusin;contentvis:oncontentvisibilityautostatechange;pageshow:onpageshow. Clients should still prefer the exacttrigger_eventsmap returned by the running server over a hard-coded copy. - trigger alias:
pageshowresolves tobody:onpageshow - custom trigger eligibility:
custom_trigger_methodslists the canonical, registered element-method tokens that may accept a validated custom event:svg,body,img,audio,video,source,input,select,button,textarea,details,iframe,animate,animmotion,set,cssanim,csstransition,link,script,form,custom,focusin, andcontentvis; thepageshowalias is intentionally excluded - capability flags:
one_shot,constructor,xor_obfuscation,source_cap_enforced,custom_extension,custom_mime_type,custom_trigger_event, andsearchable_optionsare boolean; the current built-in implementation reports all eight astrue
The server owns built-in method name, handler binding, mutation, CORS, UI group,
and exposure metadata in one typed CoreMethodSpec registry. Handler
registration, CORS projections, PING, and the bundled UI are derived from
that policy to prevent method drift.
The response also includes the header X-Ping-Response: pong. The same
metrics object is available as JSON from GET /metrics.
total_errors is retained for compatibility and matches server_errors.
client_errors counts recorded 4xx responses. server_errors counts recorded
5xx responses and exceptional request failures. Handler-returned responses and
direct error responses are included in status_counts and the matching error
bucket. Receive-layer drops before request dispatch are tracked by
receive_rejections reason and summarized under receive.rejections.
connections tracks worker-owned accepted sockets. request_admission
tracks the bounded worker budget before submission. request_latency_ms
contains in-process timing aggregates for processed request pipeline entries.
body_memory tracks the aggregate declared request-body budget, current and
peak reserved bytes, and aggregate-budget rejections.
timeouts uses low-cardinality names for receive and WebSocket timeout
signals. Accepted WebSocket upgrades are tracked through the websocket
resource counters rather than total_requests, status_counts, or
bytes_sent. Worker failures that escape normal request handling are logged
and summarized under worker.
storage.usage is refreshed with exact filesystem scans when PING or
GET /metrics builds its snapshot. uploads is aggregate regular-file usage
under uploads/, including generated SMUGGLE artifacts because they consume
the same volume; smuggle_temp is the generated-artifact subset. notes
counts encrypted .enc blobs and their bytes, not metadata sidecars.
storage.quota_denials uses only closed labels: upload byte/file/free-space/
disk-full denials, note byte/count denials, and SMUGGLE temporary byte/file
denials. advanced_upload.decode_rejections similarly uses the fixed reasons
shown above. Paths, filenames, note titles, session IDs, methods, encodings,
and exception messages never become metric labels.
storage.scans contains cumulative count, examined items, total_ms,
avg_ms, and max_ms for five fixed scopes: info, upload_quota,
notepad_usage, notepad_listing, and storage_snapshot. items is
cumulative work, not current cardinality. INFO pagination preserves exact
totals and therefore sorts/scans the directory in O(n) before slicing the
response. Aggregate upload quota checks and exact usage snapshots are also
O(n); no cache or storage index is maintained. Consequently PING and
GET /metrics are operational snapshots rather than constant-time probes.
Increase the probe interval, or use a TCP-only liveness probe when process
liveness is sufficient, if storage grows too large for frequent exact scans.
SMUGGLE¶
Create a temporary same-origin HTML/SVG/XML artifact for a file in uploads/.
The SMUGGLE request returns JSON with a temporary URL; clients then open or
download that URL to receive the generated artifact. The source file,
one-shot artifact, and extracted file are three separate things: changing the
download-facing extension or MIME metadata does not convert or validate the
embedded bytes.
Request:
Encode each path segment before adding SMUGGLE query parameters. A raw ? or
# in an upload filename changes the request path unless the filename segment
is percent-encoded first. For display and follow-up automation, use the
server-returned downloadName rather than reimplementing filename
normalization in the client.
With encryption:
encrypt=1 stores an XOR-obfuscated payload in the generated HTML page and
shows a server-generated password CAPTCHA on that page. This is obfuscation and
a manual password gate, not confidentiality or authenticated encryption.
SMUGGLE also accepts a bounded safe-builder layer for neutral internal test
artifacts. Legacy requests without these parameters stay backward-compatible.
Use locale=ru or locale=en to select localized artifact copy where the
renderer provides localized text. Omit locale to use the default ru;
unsupported locale values return a 400 SMUGGLE error with
code=invalid_smuggle_locale and field=locale.
Safe builder query parameters:
download_name: optional download-facing basename.download_ext: optional validated extracted-file suffix, at most 32 characters.txt,bin,dat,zip, andpdfare suggestions rather than an allowlist; a safe custom suffix, including a compound suffix such astar.gz, is accepted. Each ASCII segment starts with a letter or digit; its remaining characters may also contain_,+, or-. Segments are separated by single dots, and one optional leading dot is normalized away. The suffix changes only the normalized extracted filename, not the embedded bytes, outer artifact format, or MIME metadata.preset: optional fixed shell preset; one ofdirect,card_manual, orcard_auto.title: optional bounded title text rendered inside the generated page.message: optional bounded explanatory copy rendered inside the generated page.cta_label: optional bounded button label for card presets.delay_ms: optional auto-start delay in milliseconds forcard_auto(bounded to0..10000).show_notice:1or0to keep or hide the visible experimental/test-artifact notice.use_constructor:1or0to explicitly select constructor mode. An explicit false value combined with any constructor-only parameter is a conflicting configuration and returns400; the server does not silently discard those parameters. The error usescode=invalid_smuggle_configurationandfield=use_constructor.payload_encoding: constructor payload encoding; one ofb64,base64url,base32,percent,reverse,xor,hex,split,attrs, orcharcode.trigger_methodandtrigger_event: constructor trigger pair. Valid events come fromPING.smuggle_capabilities; for examplebody:onpageshowandsvg:onloadare distinct supported pairs. The discovery aliaspageshowresolves tobody:onpageshow. Built-in event sets are closed, but a customtrigger_eventis accepted when it is a bounded safe event token andtrigger_methodis one of the registered canonical element tokens incustom_trigger_methods. A custom event only attaches the handler to that generated element: the server does not synthesize or dispatch the event and does not accept raw HTML or JavaScript, so the event may never fire unless normal browser or user behavior produces it. Custom input may include or omit the leadingon; the response uses the normalizedon...form. After that prefix the token starts with an ASCII letter and contains only lowercase ASCII letters, digits,_, or-, with a total normalized limit of 64 characters.output_format: outer artifact format; one ofhtml,htm,shtml,shtm,xhtml,xht,xhtm,xml, orsvg. No additional output formats are introduced by the expanded constructor options.download_variant: constructor download implementation; one ofblob-anchor,data-uri,iframe-blob,filereader,fetch-blob,window-open,loc-assign,form-post,timeout-blob,promise-blob,raf-blob,microtask-blob,observer-blob,response-blob,readable-stream,message-channel-blob, oridle-callback-blob.page_template: constructor page shell; one ofdefault,minimal,corporate,drive,npf-zip-archive-help, ornpf-rar-archive-help.mime_type: constructor-only extracted-file Blob/data URI MIME metadata. Clients may use a value frommime_presets/mime_by_extensionor submit a validated custom MIME type. It does not inspect, validate, or convert source bytes, and it is not a simple-mode option.null_byte:1or0to prepend a leading NUL byte before the generated outer artifact bytes.
loc-assign applies the selected mime_type to its data: URL, but it cannot
force the normalized download name because no download attribute participates
in that navigation. For this variant the browser chooses any saved filename and
the response reports downloadNameApplied=false; other current variants report
true.
The safe builder remains server-authoritative: it renders only fixed neutral
test-artifact shells, keeps the normal one-shot temp-file lifecycle, and does
not allow arbitrary HTML, CSS, JavaScript, external redirects, or custom
assets. Constructor mode is incompatible with legacy encrypt=1; XOR in the
constructor payload-encoding list is only obfuscation of the embedded payload.
The title, message, cta_label, and show_notice values affect the
generated shell where the selected mode/template supports them. The legacy
npf-rar-archive-help token remains accepted for compatibility, but clients
should treat it as neutral archive-instructions copy; it does not imply a RAR
conversion or content check.
With safe builder parameters:
SMUGGLE /uploads/report.bin?download_name=Quarterly-Report&download_ext=pdf&preset=card_auto&title=Quarterly%20Report&message=Internal%20controlled%20test%20file&cta_label=Download%20test%20artifact&delay_ms=1200&show_notice=1 HTTP/1.1
Response (200):
{
"url": "/uploads/smuggle_0123abcd4567ef89.html",
"file": "report.bin",
"encrypted": false,
"downloadName": "Quarterly-Report.pdf",
"downloadNameApplied": true,
"effectiveMode": "simple",
"effectivePreset": "card_auto",
"noticeShown": true,
"locale": "ru",
"outputFormat": "html",
"payloadEncoding": "b64",
"triggerMethod": "legacy",
"triggerEvent": "legacy",
"triggerEventCustom": false,
"downloadVariant": "blob-anchor",
"pageTemplate": "legacy",
"mimeType": "application/octet-stream",
"nullByte": false
}
triggerEventCustom is true only when constructor mode accepted a custom
event token rather than one of the selected method's built-in events. It is
false for built-in events and non-constructor responses.
Headers: Content-Type: application/json, X-Smuggle-URL
X-Smuggle-URL contains the same relative path as the JSON url field. A
follow-up request to that path returns the one-shot artifact with the file data
embedded in the selected payload format. The first GET, HEAD, or matching
conditional request consumes and deletes the temporary artifact; browser
preloads, link scanners, or manual HEAD checks can therefore consume it before
the intended user opens it. Regenerating creates another independent one-shot
URL and does not invalidate any older URL that has not yet been consumed,
expired, or pruned. The server also prunes retained temporary artifacts by age,
count, and generated artifact bytes before admitting a new SMUGGLE artifact;
leftover temporary artifacts are cleaned up when the server starts.
SMUGGLE source files are capped before HTML generation. The effective cap is
the lower of the SMUGGLE source cap (10 MiB by default) and the configured
upload limit. Generated-page retention failures return 507 JSON errors and do
not publish a temporary URL.
Too large response (413):
{
"code": "smuggle_source_too_large",
"field": "source",
"error": "SMUGGLE source too large. Max size: 10.0 MB",
"status": 413,
"file_size": 10485761,
"file_size_human": "10.0 MB",
"max_size": 10485760,
"max_size_human": "10.0 MB"
}
Invalid safe-builder parameters, unsafe/overlong custom suffixes or events,
and explicit use_constructor=0 conflicts return 400 JSON errors such as
{"code": "invalid_smuggle_extension", "field": "download_ext", "error": "Invalid SMUGGLE builder extension", "status": 400}.
Current SMUGGLE code tokens are invalid_smuggle_locale,
invalid_smuggle_extension, invalid_smuggle_preset,
invalid_smuggle_payload_encoding, invalid_smuggle_trigger_method,
invalid_smuggle_trigger_event, invalid_smuggle_output_format,
invalid_smuggle_download_variant, invalid_smuggle_page_template,
invalid_smuggle_delay, invalid_smuggle_show_notice,
invalid_smuggle_null_byte, invalid_smuggle_use_constructor,
invalid_smuggle_mime_type, invalid_smuggle_configuration,
smuggle_field_too_long, smuggle_source_not_found,
smuggle_source_too_large, and smuggle_temp_quota_exceeded. Clients should
still render the human error text for operators.
Status codes: 200 OK, 400 Invalid builder params, 404 File not found, 413 Source too large, 507 Temp storage budget exhausted
NOTE¶
Secure Notepad with client-side encrypted note blobs. Clients derive an AES-256-GCM key via ECDH and the server stores only opaque encrypted data plus note metadata. This flow uses the runtime cryptography dependency and fails closed with 501 if the crypto backend is unavailable. Notes are stored in the separate top-level notes/ directory as <id>.enc + <id>.meta.json pairs, alongside uploads/ rather than inside it.
The note body field data is encrypted client-side and stored as an opaque base64 blob. Note IDs, titles, timestamps, sizes, and the optional session marker are plaintext metadata visible to the server and to any operator who can read notes/*.meta.json.
Server-generated note IDs are 32 lowercase hex characters. For legacy v0 compatibility, existing-note paths and client-supplied save IDs currently accept lowercase hex IDs from 1 to 32 characters. New clients should send 32-character lowercase hex IDs if they need client-generated IDs.
Current note keys are session-bound, not durably recoverable. The browser UI and examples/notepad_client.py keep the derived AES key only in process memory. Reloading the page, restarting the client, server restart, idle session expiry, or LRU session eviction can leave previously saved note bodies undecryptable by that client. The server does not persist note encryption keys and exposes no API to decrypt or re-key stored note blobs.
ADR-009 treats this as an intentional product/security boundary: stored ciphertext plus metadata are not sufficient for durable recovery, and the current HTTP/WebSocket Notepad flow is not a backup or multi-device sync system.
Notepad save requests have a Notepad-specific encrypted blob limit: data
must decode to at most 1 MiB (1,048,576 bytes), which is at most 1,398,104
base64 characters. Aggregate encrypted blobs are also capped by
--note-storage-limit MB and --note-count-limit N before any note temp files
are created. This application limit is enforced for both NOTE /notes and
WebSocket save messages, independent of generic transport caps such as HTTP
--max-size and the WebSocket frame limit. A lower transport cap can still
reject the request before Notepad validation runs. Per-note over-limit saves
return 413; aggregate quota failures return 507. Both HTTP and WebSocket
errors leave existing note files unchanged and do not write partial note state.
NOTE /notes/key¶
Get the server's ECDH public key.
Request:
Response (200):
If the crypto backend is unavailable, hasEcdh is false and publicKey is absent. In that mode, save/load/list/delete NOTE operations are unavailable and return 501.
NOTE /notes/exchange¶
Exchange ECDH keys to establish a session. The client sends its ephemeral P-256 public key; the server returns a short-lived sessionId and its own public key. Both sides independently derive the same AES-256-GCM session key via HKDF-SHA256.
Exact derivation contract:
| Parameter | Value |
|---|---|
| Curve | ECDH P-256 (secp256r1) |
| Public key encoding | 65-byte uncompressed X9.62 point, base64 encoded on the wire |
| HKDF hash | SHA-256 |
| HKDF output length | 32 bytes |
| HKDF salt | 32 zero bytes (00 repeated 32 times) |
| HKDF info | UTF-8 bytes for notepad-e2e-key |
| Content cipher | AES-256-GCM |
| Encrypted blob format | nonce(12) + ciphertext + tag(16), then base64 encoded as data |
Request:
NOTE /notes/exchange HTTP/1.1
Content-Type: application/json
{"clientPublicKey": "<base64 of 65-byte uncompressed P-256 point>"}
Response (200):
{
"sessionId": "<32-char hex>",
"serverPublicKey": "<base64 of 65-byte uncompressed P-256 point>",
"sessionTtlSeconds": 3600
}
sessionId is audit-only server state: if an active session ID is later sent with a save request, the server records that the note write came from a recent ECDH exchange. It is not an authorization token for reads or writes.
The complete session ID is returned only as part of this protocol response and
may be sent back by the client. Debug lifecycle messages use a stable
sidfp:<12-hex> SHA-256 fingerprint instead of the complete identifier, and
unknown-session exceptions use generic text. Operators can correlate lifecycle
events without turning logs or error bodies into a source of reusable
ephemeral session identifiers.
Status codes: 200 OK, 400 Missing/invalid key, 501 ECDH unavailable
NOTE /notes — list notes¶
Request:
Response (200):
{
"notes": [
{
"id": "<32-char hex>",
"title": "My Note",
"created_at": "2025-01-15T10:30:00+00:00",
"updated_at": "2025-01-15T10:30:00+00:00",
"size": 256
}
],
"count": 1,
"limit": 1000,
"truncated": false
}
Listing work is bounded by the configured list limit, which follows
--note-count-limit by default and falls back to 1000 when note count quota is
disabled.
NOTE /notes — save note¶
Send a JSON body to create or update a note. data must be a base64-encoded AES-256-GCM ciphertext (encrypted client-side). Include id to update an existing note; omit to create a new one. Clients that need idempotent create/retry behavior can send a valid hex id with createIfMissing: true; the server creates that note when missing and updates the same note on retry. Without createIfMissing, updating a missing id still returns 404. The encrypted blob is the source of truth, so malformed metadata sidecars are ignored or rebuilt as needed.
Request:
NOTE /notes HTTP/1.1
Content-Type: application/json
X-Session-Id: <sessionId> (optional, audit-only; ignored when expired)
{
"title": "My Note",
"data": "<base64-encoded encrypted blob>",
"id": "<32-char hex>", (omit for server-generated new note)
"createIfMissing": true (optional; only meaningful with id)
}
Response (201 for new, 200 for update):
{
"success": true,
"id": "<32-char hex>",
"title": "My Note",
"created_at": "2025-01-15T10:30:00+00:00",
"updated_at": "2025-01-15T10:30:00+00:00",
"size": 256
}
Status codes: 201 Created, 200 Updated, 400 Bad request, 404 Note not found (for update), 413 Encrypted note data too large, 507 Notepad aggregate quota exceeded, 501 Secure Notepad crypto backend unavailable
NOTE /notes/{id} — load note¶
Request:
Response (200):
{
"id": "<32-char hex>",
"title": "My Note",
"data": "<base64-encoded encrypted blob>",
"created_at": "2025-01-15T10:30:00+00:00",
"updated_at": "2025-01-15T10:30:00+00:00",
"size": 256
}
Status codes: 200 OK, 404 Not Found, 501 Secure Notepad crypto backend unavailable
NOTE /notes/{id}?delete — delete note¶
Request:
Response (200):
Status codes: 200 OK, 404 Not Found, 501 Secure Notepad crypto backend unavailable
NOTE /notes?clear=1 — clear all notes¶
Deletes all user-visible entries from the separate notes/ directory. Files in
uploads/ are not touched.
Request:
Response (200):
{
"success": true,
"cleared": true,
"path": "/notes",
"deleted_files": 4,
"deleted_dirs": 0,
"preserved": []
}
Hidden files inside notes/ are preserved.
Status codes: 200 OK, 500 Clear failed, 501 Secure Notepad crypto backend unavailable
WebSocket — /notes/ws¶
Legacy Notepad transport over WebSocket (RFC 6455). It is not a
durable workspace-sync protocol: the server does not provide revision checks,
conflict detection, replay, resume tokens, or collaborative merge. The server
detects an upgrade request on any path starting with /notes/ws and performs
the handshake inline, before the normal HTTP handler runs. The connection uses
a 60-second idle timeout; the server sends a ping frame when idle to keep the
connection alive. Active WebSocket connections are admitted against
--max-websocket-connections (--workers // 2 by default), and incomplete
frames must finish within --websocket-frame-idle-timeout (5 seconds by
default). Upgrade validation requires GET, Host,
Upgrade: websocket, Connection: Upgrade, a valid 16-byte
Sec-WebSocket-Key, and Sec-WebSocket-Version: 13. When the server is
running without the crypto backend, the upgrade is rejected with 501.
Upgrade request:
GET /notes/ws HTTP/1.1
Host: <server-host>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <base64-nonce>
Sec-WebSocket-Version: 13
Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <computed accept key>
All WebSocket messages are UTF-8 JSON text frames. The client sends masked
frames (required by RFC 6455); the server sends unmasked frames. Binary data
frames are not part of the legacy contract and are closed before dispatch with
WebSocket close code 1003 (Binary frames are not supported).
Same-origin upgrades are allowed by default. Cross-origin upgrades require the
Origin to match an exact configured --cors-origin value. Wildcard
--cors-origin * is read-only CORS and does not authorize WebSocket upgrades.
Client message types:
type |
Fields | Description |
|---|---|---|
save |
title, data, noteId?, createIfMissing?, sessionId?, opId? |
Save a note (sessionId is optional audit-only state; opId is echoed for acknowledgement correlation) |
load |
id |
Load a note by ID |
list |
— | List all notes |
delete |
id |
Delete a note while the always-on NOTE/WebSocket surface is available |
clear |
— | Clear all notes from the separate notes/ directory while the always-on NOTE/WebSocket surface is available |
Server response types:
type |
Fields | Description |
|---|---|---|
saved |
success, id, title, opId?, ... |
Note saved |
loaded |
id, title, data, ... |
Note loaded |
list |
notes, count |
Note list |
deleted |
success, id |
Note deleted |
cleared |
success, deletion counters, preserved |
Notes cleared |
error |
error |
Error message |
Domain errors for save, load, delete, and clear can keep the operation
response type (saved, loaded, deleted, or cleared) while adding
error and status. Invalid JSON, non-object messages, unknown message types,
and invalid note IDs use {"type": "error", "error": "..."}.
delete and clear are part of the same always-on NOTE/WebSocket method
surface as save, load, and list. They remain subject to the normal
authentication, origin, input-validation, and storage-safety checks; there is
no separate note_delete or note_clear runtime capability.
Unexpected internal WebSocket failures are logged as errors, counted in
metrics.websocket.errors, and closed with WebSocket code 1011 rather than
normal close code 1000. If a JSON response cannot be sent, the send failure
is logged, the connection is treated as failed, and the server attempts to close
with 1011; clients should treat the operation outcome as unknown and verify
state after reconnect.
For idempotent WebSocket saves, send opId so the client can correlate the
saved acknowledgement and send a stable hex noteId with
createIfMissing: true for first saves that may be retried after reconnect or
HTTP fallback. Retrying the same noteId updates the original note instead of
creating a duplicate. The server does not persist or deduplicate opId; it is
only copied into the corresponding saved response when present. There is no
server-side conflict contract for concurrent writes: repeated writes to the
same note ID use last-write-wins semantics at the encrypted-blob level.
OPTIONS¶
CORS preflight handler. Returns allowed methods when CORS is enabled.
Response (204): No body. If CORS is disabled, no
Access-Control-Allow-* headers are emitted. If CORS is enabled,
Access-Control-Allow-Methods lists the full core method surface for exact
origins. For wildcard --cors-origin *, preflight stays read-only and lists
only read methods. A requested unknown method is added only for exact-origin
advanced-upload preflights when it is a valid HTTP token. Requested headers are
reflected only when they are in the server allowlist (Authorization,
Content-Type, If-None-Match,
X-File-Name, X-Session-Id, X-XFerry-No-Gzip, X-Exphttp-No-Gzip, X-D, X-E, X-K,
X-Kb64, X-N, X-H, X-Encoding, X-HTTP-Method-Override,
X-Payload-In-Path, and numeric X-D-N chunk headers).
Advanced Upload¶
Advanced upload is part of the always-on full method surface. Unknown,
non-standard HTTP methods carrying advanced upload data are accepted by the
advanced upload handler. Unknown methods without an advanced payload return
shared JSON 405. Writes are still limited to uploads/.
Advanced routing control API¶
/_xferry/advanced-routing is an exact, service-owned control route:
| Method | Behavior |
|---|---|
GET |
Return current state |
PUT |
Validate and atomically replace the complete state |
PATCH |
Validate and atomically replace only diagnostic_headers |
DELETE |
Disable prefix routing and reset the decoder while preserving diagnostics |
The response state always has exactly:
State is process-local, independent per server instance, never persisted, and
resets to the values above on restart. revision increments only when the
effective configuration changes; idempotent PUT/PATCH/DELETE leaves it
unchanged. DELETE sets prefix to null and decoder to auto, preserving
the current diagnostic_headers value. Responses use Cache-Control: no-store.
PUT requires exactly all three input keys:
PUT /_xferry/advanced-routing HTTP/1.1
Content-Type: application/json
{"prefix":"/route","decoder":"auto","diagnostic_headers":false}
PATCH requires exactly the diagnostics field and never changes the route
prefix or decoder:
Prefixes are absolute and case-sensitive. Matching requires an exact path or a
slash-delimited descendant: /route matches /route and /route/file, not
/route-file. / is valid and matches every absolute path. Prefix validation
rejects query/fragment/percent/backslash/control characters, empty, . or
.. segments, trailing slash except /, and the reserved /_xferry
namespace.
Routing precedence is exact control route > matching standard upload prefix >
registered core/plugin dispatch > unknown custom-method Advanced fallback.
Only POST, PUT, PATCH, and NONE participate in prefix matching. Once
one of those methods matches, all decoder/validation errors are Advanced
responses; there is no Basic fallback. Unknown custom methods with an
Advanced payload continue to use the legacy fallback whether prefix routing is
enabled or disabled.
Supported decoder modes are auto, raw, json, text, form, xml, and
multipart. For prefix-dispatched requests, auto selects from request MIME
and treats application/octet-stream as byte-stable raw data. The configured
decoder applies only to prefix dispatch for POST, PUT, PATCH, and NONE.
In that prefix path, a fixed decoder overrides MIME; malformed or mismatched
input fails that decoder instead of silently switching. Unknown custom-method
fallback ignores the configured decoder and retains legacy MIME-derived
behavior.
Activating routing returns 409 if a configured plugin owns any of
POST/PUT/PATCH/NONE; the exact control route cannot be overridden by
root prefix or a plugin. Control authorization is deliberately narrower than
general CORS:
- unauthenticated non-browser requests require a direct loopback peer;
- an authenticated non-browser request may be remote;
- any browser request with
Originmust be same-origin; a configured CORS origin does not grant control access; - forwarded client-IP headers do not satisfy loopback;
PUT/PATCH/DELETEstill pass the normal browser mutation guard after this control-specific check.
Diagnostics¶
Basic and Advanced upload JSON diagnostics are always on. Setting
diagnostic_headers: true adds exactly these six response mirrors:
X-XFerry-HandlerX-Upload-ProfileX-File-Name-SourceX-File-SHA256X-Request-Body-SizeX-XFerry-Route-Revision
The default is off and the client controls it through the API; there are no explicit request tags that opt an individual request into Advanced routing.
The advanced upload endpoint accepts payloads through a small matrix of carriers and formats:
- body: JSON, raw binary fallback,
text/plain,application/x-www-form-urlencoded,multipart/form-data, XML/SOAP - headers:
X-Dor numeric chunksX-D-0,X-D-1, ... - query:
?d=... - cookies:
xf_d, plusxf_*metadata cookies - path: final path segment when
path_payload=1orX-Payload-In-Path: 1is present;path_filename=1treats the previous segment as the filename
Common fields are:
d/data: encoded payloade: encryption modek: decryption keykb64: whetherkis base64-encodedn/name: suggested filename; omitted names are generated as<sha256[:12]>.binh/hmac: integrity tagencoding/enc:base64,base64url,hex,percent,gzip-base64, orraw_method/method_override: method-override metadata; the receive layer does not reinterpret the actual HTTP method
For multipart-binary body uploads, the file-part filename is the
authoritative saved-name candidate and filename_source is part. Optional
filename copies in multipart n / name fields, X-N, query n / name,
xf_n / xf_name cookies, or path_filename=1 path metadata are accepted
only when every supplied copy exactly matches the file-part filename before
sanitization. Any conflict returns 400 and no file is written.
multipart-encoded continues to use d / data as the encoded payload and
n / name as filename metadata.
Form/query/JSON/XML split fields are accepted as d0, d1, ... (also
d-0, d_0, data0, data-0, data_0). Header transport also supports
chunked payload headers X-D-0, X-D-1, ... for long values.
Advanced upload applies explicit caps before writing. Encoded carriers are
admitted before payload decoding; decoded payloads are capped during gzip
decompression and after other decoders or decryption, before any publish. The
HTTP receive layer still buffers accepted request bodies and enforces the
global --max-size cap before dispatch.
- Decoded advanced-upload payloads are limited to 16 MB by default and never exceed
--max-size. - JSON body requests larger than the encoded payload cap plus a 4 KB JSON envelope allowance are rejected before UTF-8 decoding or JSON parsing. JSON bodies inside that envelope are parsed, then
d/datastrings are checked against the exact encoded-size cap before payload decoding. gzip-base64andgzip-base64urloutput is decompressed incrementally with the decoded-payload cap enforced during decompression. Expansion beyond the cap returns413; invalid gzip data remains a400invalid-encoding response.- Header transport
X-Dand combinedX-D-0,X-D-1, ... data are limited to 64 KB of encoded data by default. - URL query transport
?d=is limited to 16 KB of encoded data by default. - Cookie payload data shares the header encoded-data cap. Marked path payload data shares the URL encoded-data cap.
Transfer-Encoding: chunkedrequest bodies remain unsupported and are rejected before dispatch; use split fields or header chunks instead.
Over-limit advanced-upload requests, including gzip expansion beyond the decoded cap, return 413 JSON errors and do not write files. Larger uploads should use standard POST, PUT, PATCH, or NONE body uploads, which are governed by --max-size per request and the optional aggregate upload storage policy before publish.
The browser UI is profile-first: Managed presets configure a coherent request,
while Experimental permits explicit changes without silently rewriting the
selected carrier/profile. Multipart uses browser-managed FormData; its
top-level multipart/form-data boundary and Content-Length are authoritative,
so a conflicting declared MIME cannot be sent. A fixed decoder/profile
mismatch is reported before send. Filename placement has one primary location
plus optional exact copies, and file size produces warnings/recommendations
only—there is no automatic profile switch.
Browser Fetch cannot issue CONNECT, TRACE, or TRACK, or directly set
forbidden headers such as Host, Cookie, and Content-Length. Cookie
profiles therefore write document.cookie; multipart length/boundary remain
browser-managed. The UI models browser ceilings of 2 GiB for body, 64 KiB for
headers, 8 KiB for query/path, and 4 KiB for cookies alongside the server caps
above and the global request cap.
Resumable Content-Range uploads and protocol-specific HTTP/2 or HTTP/3
behavior are out of scope. A reverse proxy may terminate HTTP/2 externally,
but xferry's documented application/request-framing contract remains HTTP/1.1.
JSON body example:
Request:
Response (200):
The response does not include the final saved filename or /uploads/... path.
If n / name is omitted, the server uses <sha256[:12]>.bin. Before writing,
the chosen name is sanitized and, if it already exists, receives a random
suffix. Clients that need a stable follow-up path should provide a filename and
can confirm the saved workspace contents with INFO.
Header transport example:
Request:
CHECKDATA /filename HTTP/1.1
X-D: <base64-payload>
X-E: xor
X-K: <password>
X-Kb64: 0
X-N: file.bin
X-H: <hmac-sha256-hex>
<optional raw body if not using structured fields>
Authentication¶
When --auth is enabled, all requests require HTTP Basic Auth:
Failed auth returns 401 with WWW-Authenticate header. Rate limiting applies after 5 failures (30s cooldown, 429 response).
The limiter keys on the direct TCP peer IP from the accepted socket. In
proxied deployments, 401/429 semantics therefore reflect the proxy
connection unless the proxy enforces per-client throttling first. Forwarded,
X-Forwarded-For, and similar headers are not trusted as client identity; see
docs/ADR/ADR-008-trusted-proxy-client-identity-boundary.md.
Browser-Origin Mutation Guard¶
State-changing HTTP requests from browsers are accepted only when they are
same-origin or explicitly allowed by --cors-origin. Protected methods are
POST, PUT, PATCH, DELETE, NONE, NOTE, SMUGGLE, plus unknown
methods that carry advanced-upload data.
Requests with an Origin header must match the request host/scheme or a
configured CORS origin. Sec-Fetch-Site: cross-site and same-site requests
without Origin are rejected; with Origin, they require a configured CORS
origin. Non-browser API clients that omit both Origin and Sec-Fetch-Site
keep the existing behavior. Wildcard --cors-origin * still emits read CORS
headers, but does not authorize browser mutations from arbitrary origins.
Rejected browser-origin mutations return 403 JSON:
Common Headers¶
| Header | Description |
|---|---|
X-Request-Id |
Unique request correlation ID on normally dispatched HTTP responses; direct guard or upgrade errors may be sent before this decoration |
X-Upload-Status |
Upload result: success, error, no-data |
X-Fetch-Status |
Download result: success, file-not-found |
X-File-Name |
Sanitized filename |
X-File-Size |
File size in bytes |
X-File-Path |
Path to uploaded file |
X-Ping-Response |
Ping result (pong) |
X-XFerry-Handler |
Optional upload dispatch mirror: basic or advanced |
X-Upload-Profile |
Optional decoded upload profile mirror |
X-File-Name-Source |
Optional filename-source mirror |
X-File-SHA256 |
Optional final-payload SHA-256 mirror |
X-Request-Body-Size |
Optional received body-byte-count mirror |
X-XFerry-Route-Revision |
Optional process-local routing revision mirror |
X-XFerry-No-Gzip |
Request opt-out for HTTP response gzip compression (1); the canonical client header |
X-Exphttp-No-Gzip |
Legacy request alias for X-XFerry-No-Gzip |