fix(openapi): harden story 2.1 production lifecycle
This commit is contained in:
@@ -70,7 +70,16 @@ async fn parse_openapi_upload(
|
||||
.ok_or_else(|| ApiError::openapi_upload(locale, "invalid_filename"))?;
|
||||
let mime_type = field
|
||||
.content_type()
|
||||
.map(ToString::to_string)
|
||||
// Axum exposes the raw header value here. Persist only the MIME
|
||||
// essence so parameters and case cannot make route and service
|
||||
// validation disagree.
|
||||
.map(|mime| {
|
||||
mime.split(';')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
})
|
||||
.ok_or_else(|| ApiError::openapi_upload(locale, "invalid_media_type"))?;
|
||||
if !valid_upload_type(filename, &mime_type) {
|
||||
return Err(ApiError::openapi_upload(locale, "invalid_media_type"));
|
||||
@@ -123,35 +132,90 @@ fn valid_upload_type(filename: &str, mime_type: &str) -> bool {
|
||||
}
|
||||
|
||||
fn openapi_upload_locale(headers: &HeaderMap) -> OpenApiUploadLocale {
|
||||
let prefers_russian = headers
|
||||
let Some(value) = headers
|
||||
.get("accept-language")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| {
|
||||
value.split(',').any(|range| {
|
||||
let language = range.split(';').next().unwrap_or_default().trim();
|
||||
language.eq_ignore_ascii_case("ru")
|
||||
|| language.to_ascii_lowercase().starts_with("ru-")
|
||||
else {
|
||||
return OpenApiUploadLocale::En;
|
||||
};
|
||||
|
||||
// RFC 9110: highest q wins; ties preserve the header's order. Only the
|
||||
// locales served by this endpoint participate in negotiation.
|
||||
let mut preferred = (0_u16, usize::MAX, OpenApiUploadLocale::En);
|
||||
for (index, range) in value.split(',').enumerate() {
|
||||
let mut parts = range.split(';');
|
||||
let language = parts.next().unwrap_or_default().trim();
|
||||
let locale = if language.eq_ignore_ascii_case("ru")
|
||||
|| language.to_ascii_lowercase().starts_with("ru-")
|
||||
{
|
||||
Some(OpenApiUploadLocale::Ru)
|
||||
} else if language.eq_ignore_ascii_case("en")
|
||||
|| language.to_ascii_lowercase().starts_with("en-")
|
||||
|| language == "*"
|
||||
{
|
||||
Some(OpenApiUploadLocale::En)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(locale) = locale else { continue };
|
||||
let quality = match parts
|
||||
.filter_map(|parameter| {
|
||||
let (name, value) = parameter.trim().split_once('=')?;
|
||||
name.eq_ignore_ascii_case("q").then_some(value.trim())
|
||||
})
|
||||
});
|
||||
if prefers_russian {
|
||||
OpenApiUploadLocale::Ru
|
||||
} else {
|
||||
OpenApiUploadLocale::En
|
||||
.next()
|
||||
{
|
||||
None => Some(1_000_u16),
|
||||
Some(value) => value
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.filter(|quality| (0.0..=1.0).contains(quality) && *quality > 0.0)
|
||||
.map(|quality| (quality * 1_000.0).round() as u16),
|
||||
};
|
||||
let Some(quality) = quality else { continue };
|
||||
if quality > preferred.0 || (quality == preferred.0 && index < preferred.1) {
|
||||
preferred = (quality, index, locale);
|
||||
}
|
||||
}
|
||||
preferred.2
|
||||
}
|
||||
|
||||
pub async fn create_openapi_import(
|
||||
Path(path): Path<WorkspaceImportPath>,
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<OpenApiImportCreatePayload>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let imported = state
|
||||
.service
|
||||
.create_openapi_import(
|
||||
.create_openapi_import_with_locale(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.job_id.as_str().into(),
|
||||
payload,
|
||||
openapi_upload_locale(&headers),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(imported)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::http::{HeaderMap, HeaderValue};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn locale(value: &str) -> OpenApiUploadLocale {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("accept-language", HeaderValue::from_str(value).unwrap());
|
||||
openapi_upload_locale(&headers)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_language_honours_quality_zero_and_header_order() {
|
||||
assert_eq!(locale("ru;q=0, en;q=0.5"), OpenApiUploadLocale::En);
|
||||
assert_eq!(locale("en;q=0.5, ru;q=0.5"), OpenApiUploadLocale::En);
|
||||
assert_eq!(locale("ru-RU;q=0.9, en;q=1"), OpenApiUploadLocale::En);
|
||||
assert_eq!(locale("ru, en;q=0.5"), OpenApiUploadLocale::Ru);
|
||||
assert_eq!(locale("ru;q=bad, en;q=0.5"), OpenApiUploadLocale::En);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user