Skip to main content

parse_rust_server/routes/
http.rs

1//! The axum layer: extractors in, [`dispatch`] out.
2//!
3//! No behavior lives here. Each handler resolves the request context once, names the route it
4//! matched, and dispatches. `/batch` reaches the same dispatcher with the same context, which is
5//! what keeps a sub-request and a top-level request from being two implementations.
6//!
7//! **The method override is dispatched explicitly rather than rewritten by middleware.** The
8//! JavaScript SDK sends every request as a `POST` with the real method in `_method`, and axum
9//! matches on the transport method before a `Router::layer` runs. Verified by observation: a POST
10//! carrying `_method: "PUT"` produced a 405 no matter where the layer was attached. So the
11//! intended method travels in an extension and is read here. See `body_credentials`.
12
13use std::collections::HashMap;
14
15use axum::extract::{Path, Query, State};
16use axum::response::{IntoResponse, Response};
17use axum::Json;
18use serde_json::Value as Json_;
19
20use crate::auth::Authority;
21use crate::body_credentials::MethodOverride;
22use crate::params::Params;
23use crate::response::{HttpError, ParseErrorResponse};
24use crate::routes::dispatch::{self, Incoming, Route, RouteError};
25use crate::state::AppState;
26
27/// Resolve the context and run one route.
28async fn run(state: &AppState, authority: &Authority, incoming: Incoming) -> Response {
29    // **A session token attached to `/login` is discarded before it is resolved**
30    // (`middlewares.js:267-268`). Upstream deletes it in `handleParseHeaders`, after the client-key
31    // check and before any `Auth` is built, so the token is never looked up at all.
32    //
33    // Without this, a client holding an expired or revoked token cannot log back in: the generic
34    // resolver validates whatever token arrived and answers `Invalid session token` before the
35    // credentials in the body are ever read. That is the one situation where the client's only
36    // recovery is the route being refused. SDKs keep sending the stored token until a login
37    // succeeds, so the failure is self-sustaining rather than transient.
38    //
39    // Credentials are untouched: only the token is dropped. A master-key login is still a master
40    // request, and `/loginAs`, which requires master, is a different route and unaffected.
41    //
42    // Keyed on the route rather than the path string, and applied here rather than inside the
43    // login handler, so it holds for the SDK's `POST`-everything form as well. `/batch` is
44    // deliberately not covered: upstream's middleware runs once on the outer HTTP request, so a
45    // `/login` nested in a batch sees the outer request's token exactly as it does upstream.
46    let authority = &match incoming.route {
47        Route::Login => Authority {
48            session_token: None,
49            ..authority.clone()
50        },
51        _ => authority.clone(),
52    };
53
54    // One snapshot, one role expansion, per HTTP request.
55    let rc = match state.request_context(authority).await {
56        Ok(rc) => rc,
57        Err(e) => return ParseErrorResponse(e).into_response(),
58    };
59    let outcome = dispatch::dispatch(state, &rc, authority, &incoming).await;
60    match outcome {
61        Ok(response) => (response.status, Json(response.body)).into_response(),
62        Err(RouteError::Parse(e)) => ParseErrorResponse(e).into_response(),
63        Err(RouteError::Http(e)) => e.into_response(),
64        // Express answers a bare 404 for a path no router claims, with an HTML body no client
65        // parses. The status is what matters and is what a client branches on; the body is the
66        // `code`-less HTTP envelope, because inventing a Parse code for "this route does not
67        // exist" would make an absent feature look like a rejected request.
68        Err(RouteError::NotFound { method, path }) => HttpError {
69            status: http::StatusCode::NOT_FOUND,
70            message: format!("cannot route {method} {path}"),
71        }
72        .into_response(),
73    }
74}
75
76/// The method a request is really asking for.
77///
78/// An override that names a method axum would have routed differently is honoured; anything
79/// unparsable falls back to the transport method, which then fails to match a route arm and
80/// reports that rather than silently doing something else.
81fn effective_method(
82    transport: http::Method,
83    override_: Option<axum::Extension<MethodOverride>>,
84) -> http::Method {
85    match override_ {
86        Some(axum::Extension(MethodOverride(m))) => m,
87        None => transport,
88    }
89}
90
91fn params(query: HashMap<String, String>) -> Params {
92    Params::from_map(query)
93}
94
95// -------------------------------------------------------------------------------------------
96// Handlers
97// -------------------------------------------------------------------------------------------
98
99pub async fn health(State(state): State<AppState>) -> Response {
100    // Credential-free upstream, and the endpoint every bring-up script polls, so it does not go
101    // through the dispatcher's context resolution: a health check must answer while the database
102    // is unreachable, which is the state a caller most wants to distinguish.
103    let _ = state;
104    Json(crate::routes::health::body()).into_response()
105}
106
107pub async fn server_info(State(state): State<AppState>, authority: Authority) -> Response {
108    // The one route that needs no request context: it reads config and nothing else, so it stays
109    // answerable when the database is down.
110    if !authority.is_master() {
111        return HttpError::master_key_required(state.config().error_detail()).into_response();
112    }
113    Json(crate::routes::features::server_info_body(state.config())).into_response()
114}
115
116pub async fn users_collection(
117    State(state): State<AppState>,
118    authority: Authority,
119    method: Option<axum::Extension<MethodOverride>>,
120    body: Option<Json<Json_>>,
121) -> Response {
122    let method = effective_method(http::Method::POST, method);
123    run(
124        &state,
125        &authority,
126        Incoming {
127            method,
128            route: Route::Users,
129            params: Params::default(),
130            body: body.map(|b| b.0),
131            path: "/users".to_string(),
132        },
133    )
134    .await
135}
136
137pub async fn users_me(
138    State(state): State<AppState>,
139    authority: Authority,
140    method: Option<axum::Extension<MethodOverride>>,
141    transport: http::Method,
142) -> Response {
143    // The SDK reaches this as a POST carrying `_method: "GET"`.
144    let method = effective_method(transport, method);
145    run(
146        &state,
147        &authority,
148        Incoming {
149            method,
150            route: Route::UsersMe,
151            params: Params::default(),
152            body: None,
153            path: "/users/me".to_string(),
154        },
155    )
156    .await
157}
158
159pub async fn login(
160    State(state): State<AppState>,
161    authority: Authority,
162    body: Option<Json<Json_>>,
163) -> Response {
164    run(
165        &state,
166        &authority,
167        Incoming {
168            method: http::Method::POST,
169            route: Route::Login,
170            params: Params::default(),
171            body: body.map(|b| b.0),
172            path: "/login".to_string(),
173        },
174    )
175    .await
176}
177
178pub async fn logout(State(state): State<AppState>, authority: Authority) -> Response {
179    run(
180        &state,
181        &authority,
182        Incoming {
183            method: http::Method::POST,
184            route: Route::Logout,
185            params: Params::default(),
186            body: None,
187            path: "/logout".to_string(),
188        },
189    )
190    .await
191}
192
193pub async fn classes_collection(
194    State(state): State<AppState>,
195    authority: Authority,
196    Path(class_name): Path<String>,
197    Query(query): Query<HashMap<String, String>>,
198    method: Option<axum::Extension<MethodOverride>>,
199    transport: http::Method,
200    body: Option<Json<Json_>>,
201) -> Response {
202    let method = effective_method(transport, method);
203    let path = format!("/classes/{class_name}");
204    run(
205        &state,
206        &authority,
207        Incoming {
208            method,
209            route: Route::Classes { class_name },
210            params: params(query),
211            body: body.map(|b| b.0),
212            path,
213        },
214    )
215    .await
216}
217
218pub async fn classes_object(
219    State(state): State<AppState>,
220    authority: Authority,
221    Path((class_name, object_id)): Path<(String, String)>,
222    Query(query): Query<HashMap<String, String>>,
223    method: Option<axum::Extension<MethodOverride>>,
224    transport: http::Method,
225    body: Option<Json<Json_>>,
226) -> Response {
227    // There is no POST verb on an object route. A bare POST with no override used to fall through
228    // to `update`, so an unrelated request could mutate a row; an override-free POST now reaches
229    // the dispatcher as POST and finds no arm.
230    let method = effective_method(transport, method);
231    let path = format!("/classes/{class_name}/{object_id}");
232    run(
233        &state,
234        &authority,
235        Incoming {
236            method,
237            route: Route::ClassObject {
238                class_name,
239                object_id,
240            },
241            params: params(query),
242            body: body.map(|b| b.0),
243            path,
244        },
245    )
246    .await
247}
248
249pub async fn roles_collection(
250    State(state): State<AppState>,
251    authority: Authority,
252    Query(query): Query<HashMap<String, String>>,
253    method: Option<axum::Extension<MethodOverride>>,
254    transport: http::Method,
255    body: Option<Json<Json_>>,
256) -> Response {
257    let method = effective_method(transport, method);
258    run(
259        &state,
260        &authority,
261        Incoming {
262            method,
263            route: Route::Roles,
264            params: params(query),
265            body: body.map(|b| b.0),
266            path: "/roles".to_string(),
267        },
268    )
269    .await
270}
271
272pub async fn roles_object(
273    State(state): State<AppState>,
274    authority: Authority,
275    Path(object_id): Path<String>,
276    Query(query): Query<HashMap<String, String>>,
277    method: Option<axum::Extension<MethodOverride>>,
278    transport: http::Method,
279    body: Option<Json<Json_>>,
280) -> Response {
281    let method = effective_method(transport, method);
282    let path = format!("/roles/{object_id}");
283    run(
284        &state,
285        &authority,
286        Incoming {
287            method,
288            route: Route::RoleObject { object_id },
289            params: params(query),
290            body: body.map(|b| b.0),
291            path,
292        },
293    )
294    .await
295}
296
297pub async fn sessions_collection(
298    State(state): State<AppState>,
299    authority: Authority,
300    Query(query): Query<HashMap<String, String>>,
301    method: Option<axum::Extension<MethodOverride>>,
302    transport: http::Method,
303) -> Response {
304    let method = effective_method(transport, method);
305    run(
306        &state,
307        &authority,
308        Incoming {
309            method,
310            route: Route::Sessions,
311            params: params(query),
312            body: None,
313            path: "/sessions".to_string(),
314        },
315    )
316    .await
317}
318
319pub async fn sessions_me(
320    State(state): State<AppState>,
321    authority: Authority,
322    method: Option<axum::Extension<MethodOverride>>,
323    transport: http::Method,
324) -> Response {
325    let method = effective_method(transport, method);
326    run(
327        &state,
328        &authority,
329        Incoming {
330            method,
331            route: Route::SessionsMe,
332            params: Params::default(),
333            body: None,
334            path: "/sessions/me".to_string(),
335        },
336    )
337    .await
338}
339
340pub async fn sessions_object(
341    State(state): State<AppState>,
342    authority: Authority,
343    Path(object_id): Path<String>,
344    Query(query): Query<HashMap<String, String>>,
345    method: Option<axum::Extension<MethodOverride>>,
346    transport: http::Method,
347) -> Response {
348    let method = effective_method(transport, method);
349    let path = format!("/sessions/{object_id}");
350    run(
351        &state,
352        &authority,
353        Incoming {
354            method,
355            route: Route::SessionObject { object_id },
356            params: params(query),
357            body: None,
358            path,
359        },
360    )
361    .await
362}
363
364pub async fn schemas_collection(
365    State(state): State<AppState>,
366    authority: Authority,
367    method: Option<axum::Extension<MethodOverride>>,
368    transport: http::Method,
369    body: Option<Json<Json_>>,
370) -> Response {
371    let method = effective_method(transport, method);
372    run(
373        &state,
374        &authority,
375        Incoming {
376            method,
377            route: Route::Schemas,
378            params: Params::default(),
379            body: body.map(|b| b.0),
380            path: "/schemas".to_string(),
381        },
382    )
383    .await
384}
385
386pub async fn schemas_class(
387    State(state): State<AppState>,
388    authority: Authority,
389    Path(class_name): Path<String>,
390    method: Option<axum::Extension<MethodOverride>>,
391    transport: http::Method,
392    body: Option<Json<Json_>>,
393) -> Response {
394    let method = effective_method(transport, method);
395    let path = format!("/schemas/{class_name}");
396    run(
397        &state,
398        &authority,
399        Incoming {
400            method,
401            route: Route::SchemaClass { class_name },
402            params: Params::default(),
403            body: body.map(|b| b.0),
404            path,
405        },
406    )
407    .await
408}
409
410pub async fn purge(
411    State(state): State<AppState>,
412    authority: Authority,
413    Path(class_name): Path<String>,
414    method: Option<axum::Extension<MethodOverride>>,
415    transport: http::Method,
416) -> Response {
417    let method = effective_method(transport, method);
418    let path = format!("/purge/{class_name}");
419    run(
420        &state,
421        &authority,
422        Incoming {
423            method,
424            route: Route::Purge { class_name },
425            params: Params::default(),
426            body: None,
427            path,
428        },
429    )
430    .await
431}
432
433/// `POST /batch`.
434///
435/// Not routed through [`dispatch`], because a batch is the thing that *calls* the dispatcher. The
436/// context is resolved here, once, and shared by every sub-request.
437pub async fn batch(
438    State(state): State<AppState>,
439    authority: Authority,
440    body: Option<Json<Json_>>,
441) -> Response {
442    let rc = match state.request_context(&authority).await {
443        Ok(rc) => rc,
444        Err(e) => return ParseErrorResponse(e).into_response(),
445    };
446    let body = body.map(|b| b.0);
447    let mount_path = state.config().mount_path.clone();
448    match crate::routes::batch::handle(&state, &rc, &authority, &mount_path, body.as_ref()).await {
449        Ok(results) => Json(results).into_response(),
450        Err(e) => ParseErrorResponse(e).into_response(),
451    }
452}