Skip to content

Source: src/server/README.md — this page is rendered directly from the file in the repository. Edit it there.

Server — HTTP + WebSocket Host for the UI

The server is the network shell that lets the Angular UI use the slicer without bundling it into the browser. Two transports, one purpose.

HTTP for the bytes that don't fit in JSON; WebSocket for everything else.


Why two transports

A slicing job has two very different kinds of payload:

  • The mesh. Tens of MB of binary data, occasional, streamable.
  • The control flow. Small JSON messages (scene ops, slice requests, log lines, progress markers) — frequent, bidirectional, low-latency.

Trying to push 50 MB of STL through a WebSocket frame is fighting the medium. Trying to relay layer-by-layer progress over HTTP polling is fighting it the other way. The server uses each for what it's good at:


The contract

  1. One WebSocket per session, ephemeral state. The scene engine state lives in ws_session::handle_ws_session's stack. Disconnect = scene gone. Persistence is the UI's problem.
  2. All bytes go via HTTP. Mesh uploads are multipart POSTs; G-code downloads are streamed responses. The WebSocket carries only JSON.
  3. Workplate UUID and file UUID are distinct. POST /api/upload returns { ruuid, ofids: [file_uuid, ...] }. The workplate ruuid is the slicing job key; each file_uuid in ofids is one uploaded model the user can place in the scene. Slicing references files by file_uuid exclusively — there is no "request UUID is also the file ID" shortcut.
  4. The on-disk extension is the format hint. Uploads are stored as {file_uuid}.{ext} (e.g. .stl, .obj, .3mf); the slicer dispatches to the right loader from the extension. The wire protocol carries no format field.
  5. Every log line and phase timing is mirrored to the WS client. The WsLogger plugs into the same ProcessLogger interface the CLI uses, so verbose output is identical across both transports.
  6. The protocol is the source of truth, not the transport. Message shapes live in crate::ws_protocol with JsonSchema derives so the UI generates types from them.

Anatomy


Endpoint catalog

HTTP

RouteMethodPurpose
/api/uploadPOSTMultipart STL / OBJ / 3MF upload → { ruuid, ofids: [file_uuid, ...] }
/api/file/:file_uuidGETStream an uploaded model back (used by the viewer on cold reload)
/api/request/:ruuidGETWorkplate metadata: { ruuid, status, has_gcode, ofids: [{file_uuid, name}] }
/api/download/:ruuidGETStream the G-code produced for that workplate
/api/configGETReturn the fully-merged AppConfig
/api/configPATCHUpdate one config key, persist to slicer.toml
/* (anything else)GETServe the Angular bundle from ui_dir

POST /api/upload enforces a 500 MB file-size cap, only reads the field named "file" from the multipart payload, and preserves the original extension on disk so the slicer can pick the right loader without anyone having to re-encode the format hint.

WebSocket (/ws)

ClientMessage (browser → server):

typePurpose
SliceSlice a workplate: request_uuid + scene: [SceneObjectSliceDto] + settings
ListSessionsReplay history from the SQLite database
ResetAcknowledge / clear current state
SceneApply a list of SceneOpDtos to the session's SceneState
SceneSnapshotRequest the current scene state

ServerMessage (server → browser):

typePurpose
ConnectedSent once after handshake; carries CARGO_PKG_VERSION
LogMirrored info / debug / warn log line
PhaseMarkerPipeline phase start / end with elapsed time
Progresscurrent_layer / total_layers during slicing
SliceCompleteLayer count + download_url
SessionsListResult of ListSessions
SceneStateSnapshot reply for Scene / SceneSnapshot
ErrorFatal error during processing

Full schemas: src/ws_protocol.rs.


Lifecycle of one slice job


Threading model

  • Actix-web drives the HTTP and WebSocket I/O on its async runtime.
  • Slicing is CPU-bound, so handle_slice dispatches the actual work to tokio::task::spawn_blocking. WsLogger::send_log uses Sender::blocking_send for that reason — it would deadlock on an async runtime thread.
  • Database access also goes through spawn_blocking because rusqlite is synchronous. The Mutex<Connection> inside Database serialises all writes.

Configuration

The serve command's flags map onto [server] keys in slicer.toml:

FlagTOML keyDefault
--portserver.port5201
--hostserver.host0.0.0.0
--ui-dirserver.ui_dir./ui/dist/slicer-ui/browser
--work-dirserver.work_dirsystem temp directory

The serve command writes a default slicer.toml to the user config directory if none exists, then loads it via config::load_and_merge_config.


What this module deliberately does not do

  • No authentication. This is a localhost-only dev tool. Binding to 0.0.0.0 is opt-in and intended for LAN testing.
  • No multi-tenant scenes. Each WS connection has its own scene state; there is no shared workspace.
  • No persistence of scene state. The SQLite DB tracks completed jobs for re-download, not in-flight scenes.
  • No slicing logic. The server is a transport adapter around core::process_mesh and scene::SceneState.

See also