Internal HTTP protocol
The embedded frontend talks to the local Rust server through this protocol. It is for dcmview implementation, debugging, smoke tests, and maintainer automation only. It is not a stable public integration contract and can change without compatibility support.
The authoritative endpoint table, header names, and wire declarations live in src/api/contracts.rs. Generated TypeScript types, endpoint paths, and raw-frame header names live in frontend/src/generated/api-types.ts, and frontend/src/api.ts is the Svelte fetch boundary. The launch and startup contract lives in the crates/dcmview-protocol workspace member, which src/api/contracts.rs re-exports.
Authentication
Section titled “Authentication”Every request under /api requires this header unless the viewer was started with --no-token:
Authorization: Bearer <token>No endpoint is exempt, including /api/health, the CSV export, and unknown API routes, over TCP and over a Unix socket. The scheme is case-insensitive; the token is case-sensitive and compared in constant time. Cookies and query parameters are never accepted as credentials. The viewer page and the content-hashed assets/* files are public; they contain the viewer build, not DICOM data.
A missing, malformed, or incorrect header returns 401 with WWW-Authenticate: Bearer and the same JSON envelope for every API path:
{ "code": "unauthorized", "error": "missing or invalid access token: send Authorization: Bearer <token>"}The process generates one token from 32 bytes of operating-system randomness, encoded as 43 unpadded base64url characters. It is valid until the process exits, with no expiry or rotation. DCMVIEW_TOKEN fixes the value; it must be non-empty and use only A-Z a-z 0-9 - . _ ~. --no-token disables the check and warns on standard error, and setting both stops startup. The token is never accepted on the command line.
The launch URL carries the token as #token=<token>. Browsers do not send fragments in HTTP requests. The frontend moves the token from the fragment to sessionStorage and out of the address bar on load, and send() in frontend/src/api.ts adds the header. Every API resource is therefore loaded with fetch, never through an <img> or <a> URL, and downloads are fetched and then saved from a blob.
Treat the printed launch URL and the startup line as credentials. Plain HTTP does not encrypt the bearer header or DICOM data.
Startup line
Section titled “Startup line”With the hidden --startup-json option, dcmview prints one server_started JSON line on standard output once the listener is bound:
{ "type": "server_started", "url": "http://127.0.0.1:43127/#token=<token>", "base_url": "http://127.0.0.1:43127", "token": "<token>", "host": "127.0.0.1", "port": 43127, "protocol": 1}| Field | Meaning |
|---|---|
url |
Launch URL that a browser opens, with the token fragment. null for a Unix socket. |
base_url |
Origin without a fragment or trailing slash. Append API paths to this. Absent for a Unix socket. |
token |
The bearer token. null under --no-token, where url has no fragment. |
host |
The bind host as configured. Absent for a Unix socket. |
port |
The bound TCP port. Absent for a Unix socket. |
socket |
Absolute socket path under --unix-socket. Absent for TCP. |
protocol |
Version of the launch and startup contract, currently 1. Fields are only ever added. |
{ "type": "server_started", "url": null, "token": "<token>", "socket": "/run/user/1000/dcmview/scan.sock", "protocol": 1}v0.4.0 added base_url, token, protocol, and socket. A consumer that only reads url keeps working, because url carries the token. A consumer that rewrites the origin, such as a forwarded port, reads base_url and token separately and appends the fragment itself.
Call the API from a script
Section titled “Call the API from a script”Start the viewer with dcmview --no-browser --startup-json ./study. In another shell, paste its server_started line when read waits, then request the API. The example needs jq and curl:
read -r startupbase_url=$(printf '%s' "$startup" | jq -r '.base_url')token=$(printf '%s' "$startup" | jq -r '.token')printf 'Authorization: Bearer %s\n' "$token" | curl --fail --header @- "$base_url/api/files"The header is piped so that the token does not appear in curl’s arguments. Use base_url when appending API paths; never append them after the launch URL’s fragment. For a Unix socket, send the same header with curl --unix-socket <path> and an http://localhost/api/... URL.
Contract enforcement
Section titled “Contract enforcement”The endpoint table is executable rather than documentation-only:
- Rust unit tests check endpoint uniqueness and that the raw-frame header table covers exactly the serialized raw metadata fields.
- Axum integration tests request every declared endpoint with its declared method and compare status, media type, and response headers with the table, and check the shared JSON error envelope for file-scoped endpoints and boundary rejections.
cargo run --example generate_api_types -- --check, run bynpm --prefix frontend run check:contractsand every check profile, rejects generated TypeScript drift.
Regenerate the TypeScript after a contract change:
npm --prefix frontend run generate:typesFollow the change sequence in Runtime internals when adding or modifying an operation.
Response headers on every request
Section titled “Response headers on every request”X-Server-Instanceis on every API response, including errors and unknown API routes. Its value is the server’s Unix-millisecond start time, identical toserver_start_ms. A client with a loaded catalog must discard that session when a later response carries a different identity; the viewer reloads instead of mixing data from two servers. Detection rides on ordinary requests, without keepalive polling.X-Content-Type-Options: nosniffis on every response, including the viewer page, assets, and errors, so browsers never reinterpret raw samples, JSON, or CSV that quote file contents.- JSON, CSV, scripts, and styles are gzip-compressed for clients that send
Accept-Encoding: gzip. PNG frames, raw samples, and fonts are not. Content-hashed assets are cacheable.
Endpoint registry
Section titled “Endpoint registry”Static frontend assets are served at / and /assets/* without the token. Protocol operations are under /api and require it; {index} is a file index from /api/files, and {frame} is a zero-based frame. Every success status is 200.
The viewer loads its assets and calls the API with URLs relative to its page (assets/..., api/...), so a reverse proxy can serve it under a path prefix such as /user/alice/proxy/8888/. The proxy strips the prefix before forwarding and redirects the bare prefix to its trailing-slash form. The server’s routes are unchanged.
| Method | Path | Success type | Purpose |
|---|---|---|---|
| GET | /health |
JSON | Ready state, viewer name, version, build target and profile, file count, start time, and masked. |
| GET | /files |
JSON | Progressive file registry, skipped and filtered paths, and scan counters. |
| GET | /series |
JSON | Study/Series-owned logical stacks and source-frame mappings. |
| GET | /file/{index}/info |
JSON | One file’s frame, geometry, syntax, object kind, support, and default window data. |
| GET | /file/{index}/references |
JSON | Typed declared relationships and resolved local file/frame targets. |
| GET | /file/{index}/semantic-context |
JSON | SEG, Parametric Map, RT Dose, or presentation state context; otherwise not-applicable. |
| GET | /file/{index}/frame/{frame} |
image/png |
Server-rendered display frame. |
| GET | /file/{index}/frame/{frame}/raw |
application/octet-stream |
Decoded sample bytes and rendering headers. |
| GET | /file/{index}/frame/{frame}/raw/pixel |
application/octet-stream |
One pixel’s stored samples as a 1x1 raw frame. Query: row, column. |
| GET | /file/{index}/frame/{frame}/presentation-layer |
image/png |
The frame’s display shutter fill and overlay graphics as a transparent RGBA layer. |
| GET | /file/{index}/frame/{frame}/value-mapping |
JSON | Stored, Modality, real-world, and VOI LUT conversions for this frame. |
| GET | /file/{index}/frame/{frame}/segmentation-overlay |
image/png |
Transparent, source-sized SEG mask after validated source and geometry mapping. |
| GET | /file/{index}/frame/{frame}/dose-overlay |
image/png |
RT Dose colorwash sized to this frame. Query: dose. |
| GET | /file/{index}/frame/{frame}/dose-overlay/values |
application/octet-stream |
The same resampled dose as little-endian f32 values. Query: dose. |
| GET | /file/{index}/frame/{frame}/parametric-map-overlay |
image/png |
Parametric Map colorwash sized to this frame. Query: map. |
| GET | /file/{index}/frame/{frame}/parametric-map-overlay/values |
application/octet-stream |
The same resampled mapped values as little-endian f32 values. Query: map. |
| GET | /file/{index}/frame/{frame}/graphic-annotations |
JSON | Graphic and text objects one presentation state draws on this frame. Query: state. |
| GET | /file/{index}/frame/{frame}/wsi-context |
JSON | Selected WSI tile positioning, optical/focal context, companions, and warnings. |
| GET | /file/{index}/tags |
JSON | Lazy preview tag tree. |
| GET | /file/{index}/tags/select |
JSON | Direct tag-path retrieval and sequence pagination. |
| GET | /file/{index}/annotations |
JSON | Current in-memory ROI payload. |
| PUT | /file/{index}/annotations |
JSON | Replace one file’s complete ROI payload. |
| GET | /file/{index}/redactions |
JSON | One file’s redaction boxes. |
| PUT | /file/{index}/redactions |
JSON | Replace one file’s redaction boxes. |
| PUT | /file/{index}/redactions/series |
JSON | Copy the file’s redaction boxes to the same-sized files of its series. Takes no body. |
| GET | /annotations/export.csv |
CSV | Export every current annotation row. |
v0.3.0 removed viewer.build_git_sha from /health; viewer.version identifies released builds.
Progressive registry
Section titled “Progressive registry”GET /api/files returns files, discovery, server_start_ms, masked, scan_complete, scanned, skipped, and filtered. v0.3.0 removed the tunnelled and tunnel_host fields.
The files array can grow while scan_complete is false. Automation that requires a complete snapshot must poll until completion. discovery lists only skipped and filtered paths, up to the 256 most recent, each with path, disposition, and reason; accepted files appear only in files, and totals stay in the counters.
Each file reports its stable index, path and label, patient/study/series/instance identifiers, SOP Class and object kind, support state and reason, pixel and frame geometry, effective pixel aspect ratio, transfer syntax, and optional default window. display_name is the name the viewer shows outside the directory tree: the file name, or a synthetic File N in a masked session. burned_in_annotation is true when the file declares Burned In Annotation YES. frame_count is the declared Number of Frames bounded by the frames the file can actually hold, so a truncated file lists only the frames it has.
raw_windowing_compatible is always true and raw_windowing_reason is always null: the value mapping and presentation layer let a client reproduce every presentation transform. presentation_layer is true when grayscale display frames carry a display shutter or overlay graphics. Raw sample availability and browser size limits still decide which render path the viewer uses.
Logical series and references
Section titled “Logical series and references”GET /api/series groups only objects with both Study and Series Instance UIDs. Each logical stack has ordered frame records mapping a virtual index to a source file_index and source frame. v0.3.0 removed the repeated per-frame source_path; read paths from /api/files.
Geometry ordering can report missing_positions, duplicate_positions, nonuniform_spacing, inconsistent_orientation, and gantry_tilt. Enhanced concatenations and WSI pyramid levels keep typed stack identities.
Reference targets retain declared one-based frame numbers. Resolved matches contain validated zero-based frame_indices for viewer navigation. Empty matches mean a declaration was understood but no target was found in the current registry snapshot.
Display-frame query and cache
Section titled “Display-frame query and cache”GET /api/file/{index}/frame/{frame} accepts:
| Query | Accepted value |
|---|---|
wc |
Finite window center; use with ww. |
ww |
Positive finite window width; use with wc. |
mode |
default or full_dynamic. Full dynamic ignores wc, ww, and unit. |
unit |
Real-world unit that wc and ww are expressed in; requires both. |
preview |
true for a window/level drag preview that is not cached or shared. |
Window resolution order is full dynamic, explicit query pair, DICOM default, then current-frame percentile fallback. Integer Modality windows keep a width floor of 1; float, fractional Modality, and real-world windows use the continuous function without that floor.
A unit window applies to the values of the frame’s preferred real-world mapping when that mapping’s unit_label equals unit. Each stored value is mapped (linear or LUT, including non-monotonic LUTs), the window follows the LINEAR function without integer half-unit offsets, and stored values outside the mapped range take the window’s low end. The frame’s samples must be 8-bit, 16-bit, or one-bit integers. A frame whose preferred mapping has another unit, or whose samples are not such integers, is shown with its default window. unit without wc and ww returns 400 invalid_window.
Successful responses include X-Cache: HIT or X-Cache: MISS. The display cache key includes file, frame, wc, ww, mode, the unit of a real-world window, and the revision of the file’s redaction boxes; windows that render alike share a key. A request for a frame another request is already decoding waits for it and reports HIT, and a decode whose client disconnected still fills the cache. Grayscale display frames are windowed from decoded samples kept in the raw cache, so a raw request after a display request may report HIT. A preview is served from the display cache when that window is already there and is otherwise rendered for that request alone.
Display responses also describe the applied presentation:
| Header | Meaning |
|---|---|
X-Frame-Window-Applied |
linear, real_world, or voi_lut for grayscale frames, including cache hits and previews; omitted for color. |
X-Frame-Window-Center, X-Frame-Window-Width |
The linear window in Modality values that the frame was rendered with; absent for color, VOI LUT, and applied unit windows. |
Only real_world confirms that a requested unit window was applied. A fallback reports linear with the default window, or voi_lut without center and width.
Raw-frame headers
Section titled “Raw-frame headers”Successful raw responses include X-Cache and these required headers:
| Header | Meaning |
|---|---|
X-Frame-Rows |
Frame row count. |
X-Frame-Columns |
Frame column count. |
X-Frame-Bits-Allocated |
DICOM bits allocated. |
X-Frame-Pixel-Representation |
0 unsigned or 1 signed. |
X-Frame-Samples-Per-Pixel |
Samples per response pixel. |
X-Frame-Photometric-Interpretation |
Renderer photometric interpretation. |
X-Frame-Rescale-Slope |
DICOM rescale slope. |
X-Frame-Rescale-Intercept |
DICOM rescale intercept. |
X-Frame-Default-Wc and X-Frame-Default-Ww are present only when the file declares a default window. X-Frame-Padding-Low and X-Frame-Padding-High are present only for grayscale frames with Pixel Padding and give the inclusive stored-value range to exclude from automatic windows and draw black. One-bit samples are expanded to one byte each, but Bits Allocated remains 1. Integer and floating responses use little-endian sample bytes.
raw/pixel?row=&column= returns one pixel’s stored samples as a 1x1 raw frame with the same headers, in color-by-pixel order with planar and subsampled YBR_FULL_422 layouts resolved. It returns 400 outside the frame. The pixel readout uses it for frames too large to fetch whole.
The raw cache key is only file and source frame; window parameters do not change decoded sample bytes. Real World Value Mapping and Dose Grid Scaling are not applied to display or raw pixels, except that a display window with unit is applied to mapped values.
Value mapping
Section titled “Value mapping”GET /api/file/{index}/frame/{frame}/value-mapping describes one frame of any object so a client can read stored, Modality, and real-world values out of a raw frame:
stored_value_type:integer,float32, orfloat64.modality: the rescale slope, intercept, and type, plus a Modality LUT that replaces the rescale when present.real_world: validated conversions for this frame, preferred first. A frame’s own Real World Value Mapping functional group overrides the shared group, which overrides a top-level sequence. Each entry has a linear or LUTtransform, the inclusive stored range it maps, aunit_label, and the coded units and quantity. RT Dose reports Dose Grid Scaling assource: "dose_grid_scaling"withGyor the declared Dose Units. Mappings from separate Real World Value Mapping instances that reference this image follow assource: "rwvm_instance"with the instance’ssource_file_index.voi_lut: the VOI LUT the display path uses in default mode when no window is requested and no DICOM window is stored, ornull.
Presentation layer
Section titled “Presentation layer”GET /api/file/{index}/frame/{frame}/presentation-layer returns the display shutter fill and overlay graphics of a grayscale display frame as an RGBA PNG of the frame’s size: opaque gray where drawn and transparent elsewhere, or fully transparent without a shutter or overlay. It includes X-Cache. Because neither depends on the window, drawing this layer over a frame windowed in the browser gives exactly the display frame for that window.
Segmentation overlays
Section titled “Segmentation overlays”GET /api/file/{index}/frame/{frame}/segmentation-overlay accepts a zero-based SEG frame. Explicit derivation sources take precedence, followed by declared-source patient-geometry resolution.
After one compatible local source frame is validated, binary masks are treated as present/absent samples and fractional masks are scaled by Maximum Fractional Value. The service nearest-neighbor resamples the mask through patient coordinates and returns a transparent PNG with the source frame’s dimensions. Each segment is painted in its context display_color, and display_color_source is recommended_cielab, recommended_grayscale, or palette.
SEG semantic context includes warnings: string[]. A FRACTIONAL SEG with Maximum Fractional Value above 1 whose stored samples across the entire object are all 0 or 1 is drawn as binary, and its context warns about it. Discovery computes this verdict before serving the object; declared segmentation_type, segmentation_fractional_type, and maximum_fractional_value remain unchanged.
The encoded PNG is cached per SEG frame and resolved source frame, and X-Cache reports that cache. An absent, ambiguous, or incompatible mapping returns 422 semantic_mapping_unavailable. A non-SEG object returns 400, and an out-of-range SEG frame returns 404.
Value overlays
Section titled “Value overlays”dose-overlay and parametric-map-overlay draw a value volume on a displayed frame: the path names the displayed image frame, and the dose or map query names the RT Dose or Parametric Map file. The volume is a stack of parallel planes that is resampled trilinearly onto a displayed frame in the same Frame of Reference, in any orientation. Samples convert through each volume frame’s preferred value mapping.
The PNG has the displayed frame’s size and follows the context legend: evenly spaced viridis color_stops between min_value and max_value, with pixels outside the volume, without a mapped value, or at or below transparent_at_or_below left transparent. Colored pixels are opaque, and the viewer applies opacity. The /values form sends one little-endian f32 per displayed pixel, row-major, in the legend’s unit, with NaN outside the volume.
In semantic context, RT Dose and Parametric Map carry overlay eligibility, overlay_source_frames (at most 4096 covered local image frames), and legend when eligible. Parametric Map and RT Dose contexts report displayed_value_kind: "stored".
Overlay endpoints return 400 when the query names the wrong kind of object, 404 for an unknown file or out-of-range frame, 404 overlay_not_covering_frame when no pixel center of the displayed frame lies inside the volume, and 422 semantic_mapping_unavailable when the volume is ineligible or the displayed frame lies in another Frame of Reference or lacks geometry. Encoded overlays are cached per volume and displayed frame, and X-Cache reports that cache.
Graphic annotations
Section titled “Graphic annotations”GET /api/file/{index}/frame/{frame}/graphic-annotations?state= returns the annotations one Grayscale or Color Softcopy Presentation State draws on one image frame: the path names the displayed image frame, and the state query names the presentation state file. The response carries the state’s layers in Graphic Layer Order, graphics, texts, and skipped counts of objects that are not drawn (display_units, matrix_units, and malformed).
Every coordinate is a [column, row] position in image pixel units, where [0, 0] is the top-left corner of the top-left pixel. A graphic has a graphic_type of point, polyline, interpolated, circle, or ellipse, its points, and filled. A text has its text, an optional bounding_box with justification, and an optional anchor with anchor_visible. Each object names the zero-based annotation item and the layer it belongs to. A layer’s color is the sRGB of its recommended display value, or null.
A frame the state does not annotate returns empty lists. The endpoint returns 400 when state is missing or does not name a softcopy presentation state, and 404 for an unknown file or out-of-range frame. The state is parsed per request and nothing is cached.
In semantic context, a presentation state has kind: "presentation_state" with its layers, annotation items (at most 4096), annotated_frames (at most 4096 local image frames), and skipped counts.
Tag selection
Section titled “Tag selection”GET /api/file/{index}/tags/select requires path. A selector alternates DICOM tags and zero-based sequence item indices:
(0008,2218)/69/(0008,0100)For a selected sequence, offset defaults to 0 and limit defaults to 64 and must be at most 256. Direct selection reads the requested path from the file, so it is not constrained by preview-tree depth and item caps. Tag reads parse only up to Pixel Data and never read pixel values.
Binary VRs are represented by length. A value-level serialization failure appears as a typed error value so the rest of the tree can still render.
Annotation update and export
Section titled “Annotation update and export”The annotation update uses Content-Type: application/json and snake-case fields:
{ "num_roi": 2, "roi_coords": [ [120, 340, 220, 430], [400, 510, 480, 590] ], "roi_frames": [ [0, 1, 2], [5, 6] ]}PUT /api/file/{index}/annotations replaces one file’s complete in-memory payload and returns a canonicalized copy; it does not merge individual rectangles. Values that do not fit the file return 400 bad_request. The supported CSV format defines coordinates and frame rules.
Successful export uses text/csv; charset=utf-8 and Content-Disposition: attachment; filename="dcmview-annotations.csv".
Redaction boxes
Section titled “Redaction boxes”Redaction boxes are rectangles of a file’s frames that are withheld, drawn by hand over burned-in text. They use the EmbedRoiAnnotations shape and validation of the annotation update: roi_coords are [row0, column0, row1, column1] with exclusive ends, and roi_frames is empty (every box covers every frame) or lists each box’s zero-based frames. GET and PUT /api/file/{index}/redactions read and replace one file’s boxes. They live in memory for the session and are not part of the annotation CSV export.
Every endpoint that returns source pixels applies the boxes of the requested frame:
- Display frames paint them black. The boxes’ revision is part of the display cache key, so
X-CacheisMISSfor the first request after a change. - Raw frames, and
raw/pixel, fill them with one stored value: the frame’s darkest (its largest for MONOCHROME1), or black for color. Automatic windows computed from the samples are therefore unchanged. presentation-layerpaints them opaque black, over any overlay graphics.
PUT /api/file/{index}/redactions/series takes no body and copies the file’s boxes to every file of its series with the same rows and columns. It returns file_indices, the files that received them. Each copy gets the source’s frames when the file has as many frames as the source; otherwise its boxes cover every frame.
Masked sessions
Section titled “Masked sessions”A session started with --mask reports masked: true in /health and /files and masks every response for the life of the process. Masking is a display aid, not de-identification, and the masked values are derived from keys drawn at process start, so they differ between runs.
- File summaries carry a pseudonym in
patient_idandpatient_name, a shiftedstudy_date, hashed UIDs, alabelbuilt from those, and a syntheticdisplay_name(File N).pathis unchanged, because the directory tree shows it. - Every field named
*_uidor*_uidsholds a2.25.UID of a keyed hash. UIDs the standard registers (1.2.840.10008.*) are unchanged. The hash is the same in every response, including the tag tree, so identifiers still match across endpoints. Series and stackidvalues are built from hashed UIDs. - Tag values follow the masking rules: private elements (except private creators) and attributes of the PS3.15 Table E.1-1 basic profile are
[masked],PNvalues are[masked](Patient’s Name is the pseudonym),DAandDTvalues are shifted,ASvalues are capped at089Y,UIvalues are hashed, and every value inside a sequence the profile lists is treated as listed. Study Description, Series Description, and the patient characteristics (sex, age, size, weight) are kept. - A presentation state’s
content_creator_nameis[masked], itspresentation_creation_dateis shifted, and its text objects are withheld and counted inskipped.masked_text, in both semantic context andgraphic-annotations. - Slide label and overview images (Image Type value 3
LABELorOVERVIEW) reportsupport_state: unsupportedwithsupport_reason: masked_label_image, and their display, raw, andraw/pixelframe endpoints answer403 masked.
Masking does not change pixels; burned_in_annotation only reports what the file declares.
Error envelope
Section titled “Error envelope”Every protocol failure, including authentication, path, query, JSON extraction, unknown route, and wrong-method rejections, uses JSON:
{ "code": "not_found", "error": "file index out of range"}Internal automation may branch on the stable code within one release. The human-readable error names the file, frame, and cause where possible and is not a stable programmatic value.
| Status | Codes |
|---|---|
| 400 | invalid_path, invalid_query, invalid_json (malformed body), bad_request, invalid_window |
| 401 | unauthorized for a missing, malformed, or incorrect bearer token; sent with WWW-Authenticate: Bearer |
| 403 | masked for content a --mask session withholds |
| 404 | not_found, route_not_found, asset_not_found, no_pixel_data, frame_out_of_range, overlay_not_covering_frame |
| 405 | method_not_allowed |
| 413 | invalid_json for a JSON body over 2 MiB |
| 415 | invalid_json without Content-Type: application/json |
| 422 | invalid_json (valid JSON of the wrong shape), unsupported_transfer_syntax, unsupported_pixel_layout, semantic_mapping_unavailable |
| 500 | pixel_decode_failed, internal_error |
Display frames of any layout the catalog marks unsupported return 422 unsupported_pixel_layout; raw frames return it only for invalid geometry or numeric precision. A file deleted or moved after discovery returns 404 not_found naming its path. Request failures do not stop the server and are logged to standard error with their request.
Cross-origin debugging
Section titled “Cross-origin debugging”Normal builds do not enable browser access from another origin. A source developer can build with the debug-api Cargo feature:
DCMVIEW_SKIP_FRONTEND_BUILD=1 cargo run --features debug-api -- ./studyThe feature enables permissive CORS and prints a build warning. Do not enable it for ordinary research inspection or any public bind.