objectstore_server/endpoints/mod.rs
1//! Contains all HTTP endpoint handlers.
2//!
3//! This module documents the request and response shape of every route; see the [crate
4//! documentation](crate) for the layers a request passes through before reaching a handler.
5//!
6//! Scopes are encoded in the URL path using Matrix URI syntax: `org=123;project=456`. An
7//! underscore (`_`) represents empty scopes.
8//!
9//! # Object Endpoints
10//!
11//! All object operations live under the `/v1/` prefix:
12//!
13//! | Method | Path | Description |
14//! |----------|-------------------------------------------|------------------------------|
15//! | `POST` | `/v1/objects/{usecase}/{scopes}/` | Insert with server-generated key |
16//! | `GET` | `/v1/objects/{usecase}/{scopes}/{*key}` | Retrieve object |
17//! | `HEAD` | `/v1/objects/{usecase}/{scopes}/{*key}` | Retrieve metadata only |
18//! | `PUT` | `/v1/objects/{usecase}/{scopes}/{*key}` | Insert or overwrite with key |
19//! | `PATCH` | `/v1/objects/{usecase}/{scopes}/{*key}` | Extend expiry |
20//! | `DELETE` | `/v1/objects/{usecase}/{scopes}/{*key}` | Delete object |
21//! | `POST` | `/v1/objects:batch/{usecase}/{scopes}/` | Batch operations (multipart) |
22//!
23//! Object metadata travels in request and response headers; see
24//! [`objectstore_types::metadata`] for the mapping and the separate JSON update contract.
25//!
26//! GET and HEAD accept `response-content-disposition=inline|attachment` (default attachment).
27//! Response options apply only to successful object responses, including ranged GETs. See
28//! [`crate::extractors::response_options`] for validation. The object handlers construct
29//! download headers with browser safeguards and filename formatting.
30//!
31//! # Resumable Upload Endpoints
32//!
33//! A resumable upload transfers a single object across several requests.
34//! The client opens a session, declaring the object's total size and metadata upfront, and
35//! then sends the payload as a sequence of chunks at increasing byte offsets.
36//! If a chunk fails, the client can ask the server which offset it holds and continue from there,
37//! so an interrupted transfer resumes where it stopped instead of starting over.
38//! Clients are still encouraged to send the whole payload in a single request, as that's the most
39//! efficient and reliable approach.
40//! The server knows the total size from the session, so it recognizes the chunk carrying the last
41//! byte and completes the upload itself.
42//!
43//! Resumable uploads use the object endpoints above, selected by a query parameter:
44//! `upload_type=resumable` opens a session, and `session=<token>` addresses it from then on.
45//! Session creation returns an opaque token encoded once as unpadded base64url; that value can be
46//! placed directly in the `session` query parameter.
47//! The object is named by the request path as usual, and [`objectstore_types::resumable`]
48//! holds the protocol types.
49//!
50//! | Method | Path | Description |
51//! |----------|------------------------------------------------------------|----------------------------------------------|
52//! | `POST` | `/v1/objects/{usecase}/{scopes}/?upload_type=resumable` | Create session (server-generated key) |
53//! | `PUT` | `/v1/objects/{usecase}/{scopes}/{*key}?upload_type=resumable` | Create session (user-provided key) |
54//! | `PUT` | `/v1/objects/{usecase}/{scopes}/{*key}?session=<token>` | Upload a chunk, or query the offset |
55//! | `DELETE` | `/v1/objects/{usecase}/{scopes}/{*key}?session=<token>` | Cancel upload, discarding what was sent |
56//!
57//! Session creation requires an `Upload-Length` header carrying the total size of the object
58//! in bytes, takes the same metadata headers as a regular upload, and requires an empty body.
59//! It answers `200 OK` with `{"key", "session", "granularity"}`; the session field is the token
60//! to use in subsequent query parameters. `granularity` is this upload's persistence unit in bytes,
61//! or zero when no unit is imposed. Metadata is fixed at this point and does not change afterwards.
62//!
63//! Chunk uploads and offset queries share one request shape, distinguished by the
64//! `Upload-Offset` header: a byte offset submits the body as the chunk starting there, while
65//! the `*` wildcard submits an empty body and asks which offset the server holds. Both answer
66//! `204 No Content` with the authoritative `Upload-Offset` while bytes remain, and
67//! `201 Created` with `{"key"}` once the upload is complete and the object is available through
68//! the normal object endpoints. The session is terminal at that point.
69//! The offset in the response may be lower than the end of the last chunk that was sent.
70//! Backends can e.g. persist only aligned prefixes and discard the remainder, so clients must
71//! always continue from the returned offset.
72//! Non-final chunks shorter than one positive granularity unit are rejected; final chunks are
73//! exempt because they persist the remaining bytes.
74//! Every chunk requires `Content-Length`, even over HTTP/2, while creation and offset queries
75//! must not carry a request body.
76//!
77//! An offset query can finish pending backend publication work, so it requires write permission
78//! despite being read-shaped.
79//! Termination likewise needs write rather than delete permission: it releases an in-progress upload,
80//! not an object.
81//!
82//! | Status | Meaning | Client action |
83//! |--------|---------|---------------|
84//! | `400` | Malformed session token or one that cannot be decrypted or authenticated, missing `Upload-Length`, nonempty offset query, chunk exceeding the declared length, or non-final chunk shorter than the granularity | Correct the request |
85//! | `404` | The upload session is unknown or does not belong to this object | Start a new session or correct the request |
86//! | `409` | A chunk's offset does not match the authoritative offset | Query the offset and continue from there |
87//! | `410` | The session expired or was canceled | Start a new session |
88//! | `501` | The server declined the resumable upload session creation for the requested object | Fall back to a regular upload |
89//!
90//! # Internal Endpoints
91//!
92//! Internal endpoints are exempt from authentication, rate limiting, and the web concurrency
93//! limit so they remain available when the server is under load. [`is_internal_route`]
94//! identifies them.
95//!
96//! | Method | Path | Description |
97//! |--------|------|-------------|
98//! | `GET` | `/health` | Liveness probe (always returns 200) |
99//! | `GET` | `/ready` | Readiness probe (remains healthy during shutdown) |
100//! | `GET` | `/keda` | Prometheus text-format gauges for KEDA autoscaling (see [KEDA Metrics](crate#keda-metrics)) |
101//!
102//! # Code Usage
103//!
104//! Use [`routes`] to create a router with all endpoints.
105
106use axum::Router;
107
108use crate::state::ServiceState;
109
110mod batch;
111pub mod common;
112pub mod health;
113mod keda;
114mod objects;
115#[cfg(all(target_os = "linux", feature = "profiling"))]
116mod profiling;
117mod resumable;
118
119/// Returns `true` for internal endpoints that are exempt from metrics and concurrency limits.
120pub fn is_internal_route(route: &str) -> bool {
121 matches!(route, "/health" | "/ready" | "/keda") || route.starts_with("/debug/")
122}
123
124/// Returns a router with all objectstore HTTP endpoints mounted.
125///
126/// Mounts health and KEDA endpoints at the root and all object/batch
127/// endpoints under `/v1/`.
128pub fn routes() -> Router<ServiceState> {
129 let routes_v1 = Router::new()
130 .merge(objects::router())
131 .merge(batch::router());
132
133 let router = Router::new()
134 .merge(health::router())
135 .merge(keda::router())
136 .nest("/v1/", routes_v1);
137
138 std::cfg_select! {
139 all(target_os = "linux", feature = "profiling") => {
140 router.merge(profiling::router())
141 }
142 _ => { router }
143 }
144}