Skip to content

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.

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.

With the hidden --startup-json option, dcmview prints one server_started JSON line on standard output once the listener is bound:

TCP listener
{
"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.
Unix socket listener
{
"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.

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:

Terminal window
read -r startup
base_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.

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 by npm --prefix frontend run check:contracts and every check profile, rejects generated TypeScript drift.

Regenerate the TypeScript after a contract change:

Terminal window
npm --prefix frontend run generate:types

Follow the change sequence in Runtime internals when adding or modifying an operation.

  • X-Server-Instance is on every API response, including errors and unknown API routes. Its value is the server’s Unix-millisecond start time, identical to server_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: nosniff is 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.

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.

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.

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.

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.

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.

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, or float64.
  • 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 LUT transform, the inclusive stored range it maps, a unit_label, and the coded units and quantity. RT Dose reports Dose Grid Scaling as source: "dose_grid_scaling" with Gy or the declared Dose Units. Mappings from separate Real World Value Mapping instances that reference this image follow as source: "rwvm_instance" with the instance’s source_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, or null.

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.

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.

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.

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.

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.

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 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-Cache is MISS for 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-layer paints 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.

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_id and patient_name, a shifted study_date, hashed UIDs, a label built from those, and a synthetic display_name (File N). path is unchanged, because the directory tree shows it.
  • Every field named *_uid or *_uids holds a 2.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 stack id values 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], PN values are [masked] (Patient’s Name is the pseudonym), DA and DT values are shifted, AS values are capped at 089Y, UI values 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_name is [masked], its presentation_creation_date is shifted, and its text objects are withheld and counted in skipped.masked_text, in both semantic context and graphic-annotations.
  • Slide label and overview images (Image Type value 3 LABEL or OVERVIEW) report support_state: unsupported with support_reason: masked_label_image, and their display, raw, and raw/pixel frame endpoints answer 403 masked.

Masking does not change pixels; burned_in_annotation only reports what the file declares.

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.

Normal builds do not enable browser access from another origin. A source developer can build with the debug-api Cargo feature:

Terminal window
DCMVIEW_SKIP_FRONTEND_BUILD=1 cargo run --features debug-api -- ./study

The feature enables permissive CORS and prints a build warning. Do not enable it for ordinary research inspection or any public bind.