| 260 |
260 |
|
const REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
|
| 261 |
261 |
|
|
| 262 |
262 |
|
/// Routes exempt from [`REQUEST_TIMEOUT`]: ones that legitimately run long or
|
| 263 |
|
- |
/// stream open-ended bodies. git smart-HTTP (clone/pack), data + content
|
| 264 |
|
- |
/// exports, the SyncKit SSE push channel, and OTA / build artifact transfer.
|
|
263 |
+ |
/// stream open-ended bodies. git smart-HTTP (clone/pack), data exports, the
|
|
264 |
+ |
/// SyncKit SSE push channel, and OTA artifact transfer.
|
|
265 |
+ |
///
|
|
266 |
+ |
/// Anchored to real route prefixes, not substrings (PERF M-1, Run #23): the old
|
|
267 |
+ |
/// `contains("/export")` also exempted the `/dashboard/export` page and any
|
|
268 |
+ |
/// future creator-controlled path containing the token, silently widening the
|
|
269 |
+ |
/// un-timed-out surface. The `/sync/builds` substring matched no route (build
|
|
270 |
+ |
/// artifacts move over the OTA path, already covered).
|
| 265 |
271 |
|
fn timeout_exempt(path: &str) -> bool {
|
| 266 |
272 |
|
path.starts_with("/git")
|
| 267 |
|
- |
|| path.contains("/export")
|
| 268 |
|
- |
|| path.contains("/sync/subscribe")
|
| 269 |
|
- |
|| path.contains("/sync/ota")
|
| 270 |
|
- |
|| path.contains("/sync/builds")
|
|
273 |
+ |
|| path.starts_with("/api/export/")
|
|
274 |
+ |
|| path.starts_with("/api/internal/creator/export/")
|
|
275 |
+ |
|| path.starts_with("/api/sync/subscribe")
|
|
276 |
+ |
|| path.starts_with("/api/v1/sync/subscribe")
|
|
277 |
+ |
|| path.starts_with("/api/sync/ota")
|
|
278 |
+ |
|| path.starts_with("/api/v1/sync/ota")
|
| 271 |
279 |
|
}
|
| 272 |
280 |
|
|
| 273 |
281 |
|
/// Global request timeout with per-route opt-out. A single hung handler used to
|
| 293 |
301 |
|
}
|
| 294 |
302 |
|
}
|
| 295 |
303 |
|
|
|
304 |
+ |
#[cfg(test)]
|
|
305 |
+ |
mod timeout_exempt_tests {
|
|
306 |
+ |
use super::timeout_exempt;
|
|
307 |
+ |
|
|
308 |
+ |
#[test]
|
|
309 |
+ |
fn exempts_only_anchored_long_running_routes() {
|
|
310 |
+ |
// Genuinely long / streaming routes stay exempt.
|
|
311 |
+ |
for p in [
|
|
312 |
+ |
"/git/foo/bar.git/info/refs",
|
|
313 |
+ |
"/api/export/content",
|
|
314 |
+ |
"/api/internal/creator/export/sales",
|
|
315 |
+ |
"/api/sync/subscribe",
|
|
316 |
+ |
"/api/v1/sync/subscribe",
|
|
317 |
+ |
"/api/sync/ota/apps/x/releases",
|
|
318 |
+ |
"/api/v1/sync/ota/slug/macos/arm64/1.0.0",
|
|
319 |
+ |
] {
|
|
320 |
+ |
assert!(timeout_exempt(p), "{p} should be exempt");
|
|
321 |
+ |
}
|
|
322 |
+ |
// The substring-match hazard: these contain a token but must NOT be exempt.
|
|
323 |
+ |
for p in [
|
|
324 |
+ |
"/dashboard/export", // a normal page, not a long export
|
|
325 |
+ |
"/u/somecreator/export-notes", // creator-controlled slug
|
|
326 |
+ |
"/items/sync/ota-recap", // happens to contain the token mid-path
|
|
327 |
+ |
] {
|
|
328 |
+ |
assert!(!timeout_exempt(p), "{p} must not be exempt");
|
|
329 |
+ |
}
|
|
330 |
+ |
}
|
|
331 |
+ |
}
|
|
332 |
+ |
|
| 296 |
333 |
|
/// Middleware that sets security headers on all responses.
|
| 297 |
334 |
|
/// Embed routes (`/embed/`) get permissive frame headers for iframe embedding.
|
| 298 |
335 |
|
async fn security_headers_middleware(
|