pinakes-server: eliminate unwraps from response builders

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6d80e505963dfa4d117f6b33d69fc1516a6a6964
This commit is contained in:
raf 2026-03-07 16:55:43 +03:00
commit 1fe2c7998d
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
3 changed files with 67 additions and 64 deletions

View file

@ -33,35 +33,35 @@ pub fn create_router_with_tls(
.per_second(1)
.burst_size(100)
.finish()
.unwrap(),
.expect("valid global rate limit config"),
);
// Strict rate limit for login: 5 requests/min per IP
let login_governor = Arc::new(
GovernorConfigBuilder::default()
.per_second(12) // replenish one every 12 seconds
.burst_size(5)
.finish()
.unwrap(),
.per_second(12) // replenish one every 12 seconds
.burst_size(5)
.finish()
.expect("valid login rate limit config"),
);
// Rate limit for search: 10 requests/min per IP
let search_governor = Arc::new(
GovernorConfigBuilder::default()
.per_second(6) // replenish one every 6 seconds (10/min)
.burst_size(10)
.finish()
.unwrap(),
.per_second(6) // replenish one every 6 seconds (10/min)
.burst_size(10)
.finish()
.expect("valid search rate limit config"),
);
// Rate limit for streaming: 5 requests per IP (very restrictive for
// concurrent streams)
let stream_governor = Arc::new(
GovernorConfigBuilder::default()
.per_second(60) // replenish slowly (one per minute)
.burst_size(5) // max 5 concurrent connections
.finish()
.unwrap(),
.per_second(60) // replenish slowly (one per minute)
.burst_size(5) // max 5 concurrent connections
.finish()
.expect("valid stream rate limit config"),
);
// Login route with strict rate limiting
@ -501,9 +501,9 @@ pub fn create_router_with_tls(
// CORS: allow same-origin by default, plus the desktop UI origin
let cors = CorsLayer::new()
.allow_origin([
"http://localhost:3000".parse::<HeaderValue>().unwrap(),
"http://127.0.0.1:3000".parse::<HeaderValue>().unwrap(),
"tauri://localhost".parse::<HeaderValue>().unwrap(),
HeaderValue::from_static("http://localhost:3000"),
HeaderValue::from_static("http://127.0.0.1:3000"),
HeaderValue::from_static("tauri://localhost"),
])
.allow_methods([
Method::GET,

View file

@ -11,6 +11,36 @@ use uuid::Uuid;
use crate::{error::ApiError, state::AppState};
fn build_response(
content_type: &str,
body: impl Into<axum::body::Body>,
) -> Result<axum::response::Response, ApiError> {
axum::response::Response::builder()
.header("Content-Type", content_type)
.body(body.into())
.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to build response: {e}"),
))
})
}
fn build_response_with_status(
status: StatusCode,
headers: &[(&str, &str)],
body: impl Into<axum::body::Body>,
) -> Result<axum::response::Response, ApiError> {
let mut builder = axum::response::Response::builder().status(status);
for (name, value) in headers {
builder = builder.header(*name, *value);
}
builder.body(body.into()).map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to build response: {e}"),
))
})
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
@ -42,12 +72,7 @@ pub async fn hls_master_playlist(
));
}
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap(),
)
build_response("application/vnd.apple.mpegurl", playlist)
}
pub async fn hls_variant_playlist(
@ -84,12 +109,7 @@ pub async fn hls_variant_playlist(
}
playlist.push_str("#EXT-X-ENDLIST\n");
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/vnd.apple.mpegurl")
.body(axum::body::Body::from(playlist))
.unwrap(),
)
build_response("application/vnd.apple.mpegurl", playlist)
}
pub async fn hls_segment(
@ -127,21 +147,14 @@ pub async fn hls_segment(
))
})?;
return Ok(
axum::response::Response::builder()
.header("Content-Type", "video/MP2T")
.body(axum::body::Body::from(data))
.unwrap(),
);
return build_response("video/MP2T", data);
}
// Session exists but segment not ready yet
return Ok(
axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap(),
return build_response_with_status(
StatusCode::ACCEPTED,
&[("Retry-After", "2")],
"segment not yet available",
);
}
@ -200,12 +213,7 @@ pub async fn dash_manifest(
</MPD>"#
);
Ok(
axum::response::Response::builder()
.header("Content-Type", "application/dash+xml")
.body(axum::body::Body::from(mpd))
.unwrap(),
)
build_response("application/dash+xml", mpd)
}
pub async fn dash_segment(
@ -242,20 +250,13 @@ pub async fn dash_segment(
))
})?;
return Ok(
axum::response::Response::builder()
.header("Content-Type", "video/mp4")
.body(axum::body::Body::from(data))
.unwrap(),
);
return build_response("video/mp4", data);
}
return Ok(
axum::response::Response::builder()
.status(StatusCode::ACCEPTED)
.header("Retry-After", "2")
.body(axum::body::Body::from("segment not yet available"))
.unwrap(),
return build_response_with_status(
StatusCode::ACCEPTED,
&[("Retry-After", "2")],
"segment not yet available",
);
}

View file

@ -97,12 +97,14 @@ pub async fn get_subtitle_content(
SubtitleFormat::Srt => "application/x-subrip",
_ => "text/plain",
};
Ok(
axum::response::Response::builder()
.header("Content-Type", content_type)
.body(axum::body::Body::from(content))
.unwrap(),
)
axum::response::Response::builder()
.header("Content-Type", content_type)
.body(axum::body::Body::from(content))
.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to build response: {e}"),
))
})
} else {
Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(