refactor(request-id): align contracts and lock field names (#3454)

* refactor(request-id): align header log and trace contracts

* test(contract): lock external request-id field names

* chore(deps): drop unused rustfs-ecstore links

Co-Authored-By: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-06-15 01:59:11 +08:00
committed by GitHub
parent efaf07d323
commit 036741cb1c
12 changed files with 226 additions and 65 deletions
+17
View File
@@ -124,6 +124,7 @@ pub struct QuotaErrorResponse {
pub message: String,
#[serde(rename = "Resource")]
pub resource: String,
// External quota error contract follows the existing PascalCase error schema.
#[serde(rename = "RequestId")]
pub request_id: String,
#[serde(rename = "HostId")]
@@ -168,6 +169,7 @@ impl QuotaErrorResponse {
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
/// Legacy format: quota, created_at, updated_at (no quota_type)
#[test]
@@ -217,4 +219,19 @@ mod tests {
assert_eq!(q.quota, Some(1073741824));
assert_eq!(q.quota_type, QuotaType::Hard);
}
#[test]
fn quota_error_response_serializes_request_id_as_pascal_case_contract() {
let response = QuotaErrorResponse::new(
&QuotaError::InvalidConfig {
reason: "bad quota".to_string(),
},
"req-quota-123",
"host-quota-1",
);
let value = serde_json::to_value(response).expect("quota error response should serialize");
assert_eq!(value["RequestId"], Value::String("req-quota-123".to_string()));
assert!(value.get("request_id").is_none(), "quota error contract must not expose request_id");
}
}
@@ -36,6 +36,8 @@ pub struct ErrorResponse {
pub bucket_name: String,
pub key: String,
pub resource: String,
// External S3-style error response contract: keep `RequestId`.
#[serde(rename = "RequestId")]
pub request_id: String,
pub host_id: String,
pub region: String,
@@ -302,3 +304,29 @@ pub fn err_api_not_supported(message: &str) -> ErrorResponse {
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
#[test]
fn error_response_serializes_request_id_as_pascal_case_contract() {
let response = ErrorResponse {
code: S3ErrorCode::InvalidArgument,
message: "bad request".to_string(),
bucket_name: "bucket".to_string(),
key: "key".to_string(),
resource: "/bucket/key".to_string(),
request_id: "req-xml-123".to_string(),
host_id: "host-1".to_string(),
region: "us-east-1".to_string(),
server: "rustfs".to_string(),
status_code: StatusCode::BAD_REQUEST,
};
let value = serde_json::to_value(response).expect("error response should serialize");
assert_eq!(value["RequestId"], Value::String("req-xml-123".to_string()));
assert!(value.get("request_id").is_none(), "external error contract must not expose request_id");
}
}
+47
View File
@@ -134,12 +134,42 @@ impl tonic::service::Interceptor for TonicInterceptor {
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::global;
use opentelemetry::trace::{SpanContext, TraceContextExt, TraceFlags, TraceId, TraceState, TracerProvider as _};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tonic::service::Interceptor;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use tracing_subscriber::{Registry, layer::SubscriberExt};
fn ensure_test_rpc_secret() {
let _ = rustfs_credentials::GLOBAL_RUSTFS_RPC_SECRET.set("test-rpc-secret".to_string());
}
fn with_trace_parent<F>(trace_id_hex: &str, f: F)
where
F: FnOnce(),
{
global::set_text_map_propagator(TraceContextPropagator::new());
let provider = SdkTracerProvider::builder().build();
let tracer = provider.tracer("rpc-client-tests");
let subscriber = Registry::default().with(tracing_opentelemetry::layer().with_tracer(tracer));
tracing::subscriber::with_default(subscriber, || {
let span = tracing::info_span!("rpc-client-test-span");
let trace_id = TraceId::from_hex(trace_id_hex).expect("trace id should be valid hex");
let span_id = opentelemetry::trace::SpanId::from_hex("0102030405060708").expect("span id should be valid hex");
let parent = SpanContext::new(trace_id, span_id, TraceFlags::SAMPLED, true, TraceState::default());
span.set_parent(opentelemetry::Context::new().with_remote_span_context(parent))
.expect("failed to set parent context");
let _guard = span.enter();
f();
});
let _ = provider.shutdown();
}
#[test]
fn test_signature_interceptor_keeps_auth_headers() {
ensure_test_rpc_secret();
@@ -166,4 +196,21 @@ mod tests {
assert!(!v.as_encoded_bytes().is_empty());
}
}
#[test]
fn test_signature_interceptor_injects_traceparent_metadata() {
ensure_test_rpc_secret();
let mut interceptor = TonicSignatureInterceptor;
let req = tonic::Request::new(());
with_trace_parent("4bf92f3577b34da6a3ce929d0e0e4736", || {
let req = interceptor.call(req).expect("interceptor call should succeed");
let traceparent = req
.metadata()
.get("traceparent")
.and_then(|v| v.to_str().ok())
.expect("traceparent metadata should be injected");
assert!(traceparent.starts_with("00-4bf92f3577b34da6a3ce929d0e0e4736-"));
});
}
}
@@ -14,11 +14,10 @@
use http::{HeaderMap, HeaderValue};
use opentelemetry::{global, propagation::Injector, trace::TraceContextExt};
pub(crate) use rustfs_utils::http::headers::REQUEST_ID_HEADER;
use tracing::Span;
use tracing_opentelemetry::OpenTelemetrySpanExt;
pub(crate) const REQUEST_ID_HEADER: &str = "x-request-id";
struct HttpHeaderInjector<'a> {
headers: &'a mut HeaderMap,
}