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//! # Resumable Upload Endpoints
27//!
28//! A resumable upload transfers a single object across several requests.
29//! The client opens a session, declaring the object's total size and metadata upfront, and
30//! then sends the payload as a sequence of chunks at increasing byte offsets.
31//! If a chunk fails, the client can ask the server which offset it holds and continue from there,
32//! so an interrupted transfer resumes where it stopped instead of starting over.
33//! Clients are still encouraged to send the whole payload in a single request, as that's the most
34//! efficient and reliable approach.
35//! The server knows the total size from the session, so it recognizes the chunk carrying the last
36//! byte and completes the upload itself.
37//!
38//! Resumable uploads use the object endpoints above, selected by a query parameter:
39//! `upload_type=resumable` opens a session, and `session=<token>` addresses it from then on.
40//! Session creation returns an opaque token encoded once as unpadded base64url; that value can be
41//! placed directly in the `session` query parameter.
42//! The object is named by the request path as usual, and [`objectstore_types::resumable`]
43//! holds the protocol types.
44//!
45//! | Method | Path | Description |
46//! |----------|------------------------------------------------------------|----------------------------------------------|
47//! | `POST` | `/v1/objects/{usecase}/{scopes}/?upload_type=resumable` | Create session (server-generated key) |
48//! | `PUT` | `/v1/objects/{usecase}/{scopes}/{*key}?upload_type=resumable` | Create session (user-provided key) |
49//! | `PUT` | `/v1/objects/{usecase}/{scopes}/{*key}?session=<token>` | Upload a chunk, or query the offset |
50//! | `DELETE` | `/v1/objects/{usecase}/{scopes}/{*key}?session=<token>` | Cancel upload, discarding what was sent |
51//!
52//! Session creation requires an `Upload-Length` header carrying the total size of the object
53//! in bytes, takes the same metadata headers as a regular upload, and requires an empty body.
54//! It answers `200 OK` with `{"key", "session", "granularity"}`; the session field is the token
55//! to use in subsequent query parameters. `granularity` is this upload's persistence unit in bytes,
56//! or zero when no unit is imposed. Metadata is fixed at this point and does not change afterwards.
57//!
58//! Chunk uploads and offset queries share one request shape, distinguished by the
59//! `Upload-Offset` header: a byte offset submits the body as the chunk starting there, while
60//! the `*` wildcard submits an empty body and asks which offset the server holds. Both answer
61//! `204 No Content` with the authoritative `Upload-Offset` while bytes remain, and
62//! `201 Created` with `{"key"}` once the upload is complete and the object is available through
63//! the normal object endpoints. The session is terminal at that point.
64//! The offset in the response may be lower than the end of the last chunk that was sent.
65//! Backends can e.g. persist only aligned prefixes and discard the remainder, so clients must
66//! always continue from the returned offset.
67//! Non-final chunks shorter than one positive granularity unit are rejected; final chunks are
68//! exempt because they persist the remaining bytes.
69//! Every chunk requires `Content-Length`, even over HTTP/2, while creation and offset queries
70//! must not carry a request body.
71//!
72//! An offset query can finish pending backend publication work, so it requires write permission
73//! despite being read-shaped.
74//! Termination likewise needs write rather than delete permission: it releases an in-progress upload,
75//! not an object.
76//!
77//! | Status | Meaning | Client action |
78//! |--------|---------|---------------|
79//! | `400` | Malformed session token, missing `Upload-Length`, nonempty offset query, chunk exceeding the declared length, or non-final chunk shorter than the granularity | Correct the request |
80//! | `404` | The upload session is unknown or does not belong to this object | Start a new session or correct the request |
81//! | `409` | A chunk's offset does not match the authoritative offset | Query the offset and continue from there |
82//! | `410` | The session expired or was canceled | Start a new session |
83//! | `501` | The server declined the resumable upload session creation for the requested object | Fall back to a regular upload |
84//!
85//! # Multipart Upload Endpoints
86//!
87//! Multipart uploads are being replaced by [resumable
88//! uploads](#resumable-upload-endpoints) and will be removed once all consumers have
89//! migrated. See [`objectstore_types::multipart`] for the protocol types.
90//!
91//! | Method | Path | Description |
92//! |-----------|--------------------------------------------------------------|--------------------------------------|
93//! | `POST` | `/v1/objects:multipart/{usecase}/{scopes}/` | Initiate upload (server-generated key) |
94//! | `PUT` | `/v1/objects:multipart/{usecase}/{scopes}/{*key}` | Initiate upload (user-provided key) |
95//! | `PUT` | `/v1/objects:multipart:parts/{usecase}/{scopes}/{*key}` | Upload a part (`upload_id`, `part_number` query params) |
96//! | `GET` | `/v1/objects:multipart:parts/{usecase}/{scopes}/{*key}` | List uploaded parts (`upload_id` query param) |
97//! | `POST` | `/v1/objects:multipart:complete/{usecase}/{scopes}/{*key}` | Complete upload (`upload_id` query param) |
98//! | `DELETE` | `/v1/objects:multipart/{usecase}/{scopes}/{*key}` | Abort upload (`upload_id` query param) |
99//!
100//! The initiate POST endpoint accepts both trailing-slash and non-trailing-slash forms.
101//!
102//! The complete endpoint returns `200 OK` immediately, with a streaming body that will
103//! contain the error (if any) as JSON. Whitespace is sent in the streaming body to keep the
104//! connection open. Clients must parse the body to determine the actual outcome, and not rely
105//! on the status code.
106//!
107//! # Internal Endpoints
108//!
109//! Internal endpoints are exempt from authentication, rate limiting, and the web concurrency
110//! limit so they remain available when the server is under load. [`is_internal_route`]
111//! identifies them.
112//!
113//! | Method | Path | Description |
114//! |--------|------|-------------|
115//! | `GET` | `/health` | Liveness probe (always returns 200) |
116//! | `GET` | `/ready` | Readiness probe (returns 503 when `/tmp/objectstore.down` exists, enabling graceful drain) |
117//! | `GET` | `/keda` | Prometheus text-format gauges for KEDA autoscaling (see [KEDA Metrics](crate#keda-metrics)) |
118//!
119//! # Code Usage
120//!
121//! Use [`routes`] to create a router with all endpoints.
122
123use axum::Router;
124
125use crate::state::ServiceState;
126
127mod batch;
128pub mod common;
129pub mod health;
130mod keda;
131mod multipart;
132mod objects;
133#[cfg(all(target_os = "linux", feature = "profiling"))]
134mod profiling;
135mod resumable;
136
137/// Returns `true` for internal endpoints that are exempt from metrics and concurrency limits.
138pub fn is_internal_route(route: &str) -> bool {
139 matches!(route, "/health" | "/ready" | "/keda") || route.starts_with("/debug/")
140}
141
142/// Returns a router with all objectstore HTTP endpoints mounted.
143///
144/// Mounts health and KEDA endpoints at the root and all object/batch
145/// endpoints under `/v1/`.
146pub fn routes() -> Router<ServiceState> {
147 let routes_v1 = Router::new()
148 .merge(objects::router())
149 .merge(batch::router())
150 .merge(multipart::router());
151
152 let router = Router::new()
153 .merge(health::router())
154 .merge(keda::router())
155 .nest("/v1/", routes_v1);
156
157 std::cfg_select! {
158 all(target_os = "linux", feature = "profiling") => {
159 router.merge(profiling::router())
160 }
161 _ => { router }
162 }
163}