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