From e95fd91b30eeea647ecb304834f7dd0f573dc955 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 13 May 2026 09:02:31 -0500 Subject: [PATCH 1/2] Add focused coverage for production readiness test gaps --- .../src/auction/formats.rs | 406 ++++++++++++++++++ crates/trusted-server-core/src/error.rs | 106 +++++ .../trusted-server-core/src/host_rewrite.rs | 103 +++++ crates/trusted-server-core/src/tsjs.rs | 122 ++++++ 4 files changed, 737 insertions(+) diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index 5237921a..a5b08c3d 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -318,3 +318,409 @@ pub fn convert_to_openrtb_response( .with_header(HEADER_X_TS_EC_FRESH, &auction_request.user.fresh_id) .with_body(body_bytes)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::auction::context::ContextValue; + use crate::auction::orchestrator::OrchestrationResult; + use crate::auction::types::{AuctionResponse, Bid, BidStatus}; + use crate::platform::test_support::noop_services; + use crate::test_support::tests::create_test_settings; + use fastly::http::HeaderValue; + use serde_json::json; + use std::collections::{HashMap, HashSet}; + + fn make_request() -> Request { + let mut req = Request::new("POST", "https://publisher.example/auction"); + req.set_header( + header::USER_AGENT, + HeaderValue::from_str("Mozilla/5.0 test").expect("should create user-agent header"), + ); + req + } + + fn make_auction_request() -> AuctionRequest { + AuctionRequest { + id: "auction-1".to_string(), + slots: vec![AdSlot { + id: "div-gpt-top".to_string(), + formats: vec![AdFormat { + media_type: MediaType::Banner, + width: 300, + height: 250, + }], + floor_price: None, + targeting: HashMap::new(), + bidders: HashMap::new(), + }], + publisher: PublisherInfo { + domain: "test-publisher.com".to_string(), + page_url: Some("https://test-publisher.com".to_string()), + }, + user: UserInfo { + id: "synthetic-id".to_string(), + fresh_id: "fresh-id".to_string(), + consent: Some(ConsentContext::default()), + }, + device: None, + site: None, + context: HashMap::new(), + } + } + + fn make_bid(slot_id: &str, bidder: &str, price: Option) -> Bid { + Bid { + slot_id: slot_id.to_string(), + price, + currency: "USD".to_string(), + creative: Some("
Ad
".to_string()), + adomain: Some(vec!["advertiser.example".to_string()]), + bidder: bidder.to_string(), + width: 300, + height: 250, + nurl: None, + burl: None, + metadata: HashMap::new(), + } + } + + fn make_result(bid: Bid) -> OrchestrationResult { + OrchestrationResult { + provider_responses: vec![AuctionResponse { + provider: "prebid".to_string(), + bids: vec![bid.clone()], + status: BidStatus::Success, + response_time_ms: 42, + metadata: HashMap::new(), + }], + mediator_response: None, + winning_bids: HashMap::from([(bid.slot_id.clone(), bid)]), + total_time_ms: 50, + metadata: HashMap::new(), + } + } + + fn response_json(mut response: Response) -> JsonValue { + serde_json::from_slice(&response.take_body_bytes()).expect("should parse JSON response") + } + + #[test] + fn convert_tsjs_to_auction_request_maps_banner_units_bidders_context_and_request_metadata() { + let mut settings = create_test_settings(); + settings.auction.allowed_context_keys = HashSet::from([ + "segments".to_string(), + "lockr_id".to_string(), + "count".to_string(), + "unsupported".to_string(), + ]); + let req = make_request(); + let body = AdRequest { + ad_units: vec![AdUnit { + code: "div-gpt-top".to_string(), + media_types: Some(MediaTypes { + banner: Some(BannerUnit { + sizes: vec![vec![300, 250], vec![728, 90]], + }), + }), + bids: Some(vec![ + BidConfig { + bidder: "appnexus".to_string(), + params: json!({ "placementId": 123 }), + }, + BidConfig { + bidder: "rubicon".to_string(), + params: json!({ "accountId": 456 }), + }, + ]), + }], + config: Some(json!({ + "segments": ["seg-a", "seg-b"], + "lockr_id": "lockr-123", + "count": 2, + "unsupported": { "nested": true }, + "blocked": "drop-me" + })), + }; + + let services = noop_services(); + let auction_request = convert_tsjs_to_auction_request( + &body, + &settings, + &services, + &req, + ConsentContext::default(), + "existing-ec-id", + None, + ) + .expect("should convert banner request"); + + assert_eq!(auction_request.slots.len(), 1, "should create one slot"); + let slot = &auction_request.slots[0]; + assert_eq!(slot.id, "div-gpt-top", "should preserve ad unit code"); + assert_eq!( + slot.formats, + vec![ + AdFormat { + width: 300, + height: 250, + media_type: MediaType::Banner, + }, + AdFormat { + width: 728, + height: 90, + media_type: MediaType::Banner, + }, + ], + "should convert banner sizes to formats" + ); + assert_eq!( + slot.bidders.get("appnexus"), + Some(&json!({ "placementId": 123 })), + "should preserve bidder params" + ); + assert_eq!( + slot.bidders.get("rubicon"), + Some(&json!({ "accountId": 456 })), + "should preserve all bidder params" + ); + assert_eq!( + auction_request.publisher.domain, settings.publisher.domain, + "should copy publisher domain" + ); + assert_eq!( + auction_request + .site + .as_ref() + .map(|site| site.domain.as_str()), + Some(settings.publisher.domain.as_str()), + "should create site metadata from settings" + ); + assert_eq!( + auction_request.user.id, "existing-ec-id", + "should use caller-provided EC ID" + ); + assert!( + !auction_request.user.fresh_id.is_empty(), + "should generate a fresh EC ID" + ); + assert!( + auction_request.user.consent.is_some(), + "should preserve consent context" + ); + assert_eq!( + auction_request + .device + .as_ref() + .and_then(|device| device.user_agent.as_deref()), + Some("Mozilla/5.0 test"), + "should copy user-agent into device info" + ); + assert_eq!( + auction_request.context.get("segments"), + Some(&ContextValue::StringList(vec![ + "seg-a".to_string(), + "seg-b".to_string() + ])), + "should keep allowed string-list context values" + ); + assert_eq!( + auction_request.context.get("lockr_id"), + Some(&ContextValue::Text("lockr-123".to_string())), + "should keep allowed text context values" + ); + assert_eq!( + auction_request.context.get("count"), + Some(&ContextValue::Number(2.0)), + "should keep allowed number context values" + ); + assert!( + !auction_request.context.contains_key("unsupported"), + "should drop allowed keys with unsupported value types" + ); + assert!( + !auction_request.context.contains_key("blocked"), + "should drop disallowed context keys" + ); + } + + #[test] + fn convert_tsjs_to_auction_request_rejects_banner_sizes_that_are_not_width_height_pairs() { + let settings = create_test_settings(); + let req = make_request(); + let body = AdRequest { + ad_units: vec![AdUnit { + code: "div-gpt-top".to_string(), + media_types: Some(MediaTypes { + banner: Some(BannerUnit { + sizes: vec![vec![300, 250, 1]], + }), + }), + bids: None, + }], + config: None, + }; + + let services = noop_services(); + let err = convert_tsjs_to_auction_request( + &body, + &settings, + &services, + &req, + ConsentContext::default(), + "existing-ec-id", + None, + ) + .expect_err("should reject malformed banner size"); + + assert!( + format!("{err:?}").contains("Invalid banner size; expected [width, height]"), + "should explain invalid banner size" + ); + } + + #[test] + fn convert_tsjs_to_auction_request_skips_units_without_banner_media() { + let settings = create_test_settings(); + let req = make_request(); + let body = AdRequest { + ad_units: vec![ + AdUnit { + code: "no-media".to_string(), + media_types: None, + bids: None, + }, + AdUnit { + code: "no-banner".to_string(), + media_types: Some(MediaTypes { banner: None }), + bids: None, + }, + ], + config: None, + }; + + let services = noop_services(); + let auction_request = convert_tsjs_to_auction_request( + &body, + &settings, + &services, + &req, + ConsentContext::default(), + "existing-ec-id", + None, + ) + .expect("should skip unsupported media units"); + + assert!( + auction_request.slots.is_empty(), + "should only create slots for banner media" + ); + } + + #[test] + fn convert_to_openrtb_response_serializes_winning_bid_headers_and_orchestrator_ext() { + let settings = create_test_settings(); + let auction_request = make_auction_request(); + let result = make_result(make_bid("div-gpt-top", "appnexus", Some(2.75))); + + let response = convert_to_openrtb_response(&result, &settings, &auction_request) + .expect("should convert auction result to OpenRTB response"); + + assert_eq!(response.get_status(), StatusCode::OK, "should return OK"); + assert_eq!( + response.get_header_str(header::CONTENT_TYPE), + Some("application/json"), + "should set JSON content type" + ); + assert_eq!( + response.get_header_str(HEADER_X_TS_EC), + Some("synthetic-id"), + "should set EC ID header" + ); + assert_eq!( + response.get_header_str(HEADER_X_TS_EC_FRESH), + Some("fresh-id"), + "should set fresh EC ID header" + ); + + let json = response_json(response); + assert_eq!(json["id"], json!("auction-1"), "should preserve auction ID"); + assert_eq!( + json["seatbid"][0]["seat"], + json!("appnexus"), + "should use bidder as seat" + ); + let bid = &json["seatbid"][0]["bid"][0]; + assert_eq!(bid["id"], json!("appnexus-div-gpt-top")); + assert_eq!(bid["impid"], json!("div-gpt-top")); + assert_eq!(bid["price"], json!(2.75)); + assert_eq!(bid["adm"], json!("
Ad
")); + assert_eq!(bid["crid"], json!("appnexus-creative")); + assert_eq!(bid["w"], json!(300)); + assert_eq!(bid["h"], json!(250)); + assert_eq!(bid["adomain"], json!(["advertiser.example"])); + assert_eq!( + json["ext"]["orchestrator"]["strategy"], + json!("parallel_only"), + "should use default parallel-only strategy" + ); + assert_eq!(json["ext"]["orchestrator"]["providers"], json!(1)); + assert_eq!(json["ext"]["orchestrator"]["total_bids"], json!(1)); + assert_eq!(json["ext"]["orchestrator"]["time_ms"], json!(50)); + assert_eq!( + json["ext"]["orchestrator"]["provider_details"][0]["name"], + json!("prebid"), + "should include provider summary details" + ); + } + + #[test] + fn convert_to_openrtb_response_uses_parallel_mediation_when_mediator_configured() { + let mut settings = create_test_settings(); + settings.auction.mediator = Some("adserver_mock".to_string()); + let auction_request = make_auction_request(); + let result = make_result(make_bid("div-gpt-top", "appnexus", Some(2.75))); + + let response = convert_to_openrtb_response(&result, &settings, &auction_request) + .expect("should convert auction result to OpenRTB response"); + let json = response_json(response); + + assert_eq!( + json["ext"]["orchestrator"]["strategy"], + json!("parallel_mediation"), + "should use mediation strategy when mediator is configured" + ); + } + + #[test] + fn convert_to_openrtb_response_errors_when_winning_bid_has_no_price() { + let settings = create_test_settings(); + let auction_request = make_auction_request(); + let result = make_result(make_bid("div-gpt-top", "appnexus", None)); + + let err = convert_to_openrtb_response(&result, &settings, &auction_request) + .expect_err("should reject winning bid without decoded price"); + + assert!( + format!("{err:?}").contains("has no decoded price"), + "should explain missing decoded price" + ); + } + + #[test] + fn convert_to_openrtb_response_omits_out_of_range_dimensions() { + let settings = create_test_settings(); + let auction_request = make_auction_request(); + let mut bid = make_bid("div-gpt-top", "appnexus", Some(2.75)); + bid.width = u32::MAX; + let result = make_result(bid); + + let response = convert_to_openrtb_response(&result, &settings, &auction_request) + .expect("should convert bid with out-of-range OpenRTB width"); + let json = response_json(response); + let bid = &json["seatbid"][0]["bid"][0]; + + assert!(bid.get("w").is_none(), "should omit out-of-range width"); + assert_eq!(bid["h"], json!(250), "should keep in-range height"); + } +} diff --git a/crates/trusted-server-core/src/error.rs b/crates/trusted-server-core/src/error.rs index 2720e88b..7f64f90f 100644 --- a/crates/trusted-server-core/src/error.rs +++ b/crates/trusted-server-core/src/error.rs @@ -200,4 +200,110 @@ mod tests { }; assert_eq!(error.user_message(), "Invalid header value"); } + + #[test] + fn status_code_maps_each_error_variant_to_expected_http_response() { + let cases = vec![ + ( + TrustedServerError::BadRequest { + message: "bad input".to_string(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::GdprConsent { + message: "missing consent".to_string(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::InvalidHeaderValue { + message: "invalid header".to_string(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::Forbidden { + message: "not allowed".to_string(), + }, + StatusCode::FORBIDDEN, + ), + ( + TrustedServerError::AllowlistViolation { + host: "evil.example".to_string(), + }, + StatusCode::FORBIDDEN, + ), + ( + TrustedServerError::Configuration { + message: "config failed".to_string(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::Settings { + message: "settings failed".to_string(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::InvalidUtf8 { + message: "invalid utf-8".to_string(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::Ec { + message: "ec failed".to_string(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::KvStore { + store_name: "store".to_string(), + message: "kv failed".to_string(), + }, + StatusCode::SERVICE_UNAVAILABLE, + ), + ( + TrustedServerError::Auction { + message: "auction failed".to_string(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Gam { + message: "gam failed".to_string(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Prebid { + message: "prebid failed".to_string(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Integration { + integration: "test".to_string(), + message: "integration failed".to_string(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Proxy { + message: "proxy failed".to_string(), + }, + StatusCode::BAD_GATEWAY, + ), + ]; + + for (error, expected) in cases { + assert_eq!( + error.status_code(), + expected, + "should map {error:?} to expected HTTP status" + ); + } + } } diff --git a/crates/trusted-server-core/src/host_rewrite.rs b/crates/trusted-server-core/src/host_rewrite.rs index cb15f407..5ee49505 100644 --- a/crates/trusted-server-core/src/host_rewrite.rs +++ b/crates/trusted-server-core/src/host_rewrite.rs @@ -47,3 +47,106 @@ pub(crate) fn rewrite_bare_host_at_boundaries( out.push_str(&text[search..]); Some(out) } + +#[cfg(test)] +mod tests { + use super::*; + + const ORIGIN_HOST: &str = "origin.example.com"; + const REQUEST_HOST: &str = "proxy.example.com"; + + fn assert_rewrite(input: &str, expected: &str) { + assert_eq!( + rewrite_bare_host_at_boundaries(input, ORIGIN_HOST, REQUEST_HOST), + Some(expected.to_string()), + "should rewrite bare host at valid boundaries" + ); + } + + fn assert_no_rewrite(input: &str) { + assert_eq!( + rewrite_bare_host_at_boundaries(input, ORIGIN_HOST, REQUEST_HOST), + None, + "should not rewrite host embedded in a larger host token" + ); + } + + #[test] + fn returns_none_when_origin_or_request_host_is_empty() { + assert_eq!( + rewrite_bare_host_at_boundaries("origin.example.com", "", REQUEST_HOST), + None, + "should ignore empty origin host" + ); + assert_eq!( + rewrite_bare_host_at_boundaries("origin.example.com", ORIGIN_HOST, ""), + None, + "should ignore empty request host" + ); + } + + #[test] + fn returns_none_when_origin_host_is_absent() { + assert_no_rewrite("https://other.example.com/news"); + } + + #[test] + fn rewrites_exact_bare_host() { + assert_rewrite("origin.example.com", "proxy.example.com"); + } + + #[test] + fn rewrites_bare_host_with_path_query_and_fragment() { + assert_rewrite( + "origin.example.com/news?x=1#top", + "proxy.example.com/news?x=1#top", + ); + } + + #[test] + fn rewrites_multiple_valid_occurrences() { + assert_rewrite( + "origin.example.com/a and origin.example.com/b", + "proxy.example.com/a and proxy.example.com/b", + ); + } + + #[test] + fn rewrites_hosts_surrounded_by_punctuation_and_whitespace() { + assert_rewrite( + r#"{"host":"origin.example.com", "next": (origin.example.com) }"#, + r#"{"host":"proxy.example.com", "next": (proxy.example.com) }"#, + ); + } + + #[test] + fn does_not_rewrite_subdomains_or_embedded_prefixes() { + assert_no_rewrite("cdn.origin.example.com"); + assert_no_rewrite("notorigin.example.com"); + assert_no_rewrite("foo-origin.example.com"); + } + + #[test] + fn does_not_rewrite_suffix_domains_or_host_char_continuations() { + assert_no_rewrite("origin.example.com.uk"); + assert_no_rewrite("origin.example.com-prod"); + } + + #[test] + fn rewrites_origin_host_with_port_when_origin_includes_port() { + assert_eq!( + rewrite_bare_host_at_boundaries( + "origin.example.com:8443/path", + "origin.example.com:8443", + REQUEST_HOST, + ), + Some("proxy.example.com/path".to_string()), + "should rewrite host and port when origin host includes the port" + ); + } + + #[test] + fn does_not_rewrite_host_with_port_when_origin_omits_port() { + assert_no_rewrite("origin.example.com:8443/path"); + } +} diff --git a/crates/trusted-server-core/src/tsjs.rs b/crates/trusted-server-core/src/tsjs.rs index 4921a9bb..f79d5bb4 100644 --- a/crates/trusted-server-core/src/tsjs.rs +++ b/crates/trusted-server-core/src/tsjs.rs @@ -64,3 +64,125 @@ pub fn tsjs_deferred_script_tags(module_ids: &[&str]) -> String { .map(|id| tsjs_deferred_script_tag(id)) .collect::() } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_query_value(src: &str) -> &str { + src.split_once("?v=") + .map(|(_, hash)| hash) + .expect("should contain cache-busting hash query") + } + + fn assert_sha256_hex_hash(value: &str) { + assert_eq!(value.len(), 64, "should be a SHA-256 hex digest"); + assert!( + value.chars().all(|ch| ch.is_ascii_hexdigit()), + "should contain only ASCII hex digits" + ); + } + + #[test] + fn tsjs_script_src_formats_unified_bundle_url_with_hash() { + let src = tsjs_script_src(&["creative"]); + + assert!( + src.starts_with("/static/tsjs=tsjs-unified.min.js?v="), + "should use unified static bundle path" + ); + assert_sha256_hex_hash(hash_query_value(&src)); + } + + #[test] + fn tsjs_script_src_hash_changes_with_module_set() { + let creative_src = tsjs_script_src(&["creative"]); + let creative_prebid_src = tsjs_script_src(&["creative", "prebid"]); + + assert_ne!( + creative_src, creative_prebid_src, + "should include requested modules in cache-busting hash" + ); + } + + #[test] + fn tsjs_script_tag_wraps_source_in_single_trustedserver_tag() { + let module_ids = ["creative"]; + let src = tsjs_script_src(&module_ids); + + assert_eq!( + tsjs_script_tag(&module_ids), + format!(""), + "should generate exactly one trusted server script tag" + ); + } + + #[test] + fn tsjs_unified_helpers_use_all_module_ids() { + let ids = all_module_ids(); + + assert_eq!( + tsjs_unified_script_src(), + tsjs_script_src(&ids), + "should hash all module IDs for the unified script source" + ); + assert_eq!( + tsjs_unified_script_tag(), + tsjs_script_tag(&ids), + "should wrap the all-module unified script source" + ); + } + + #[test] + fn tsjs_deferred_script_src_formats_known_module_url_with_hash() { + let src = tsjs_deferred_script_src("prebid"); + + assert!( + src.starts_with("/static/tsjs=tsjs-prebid.min.js?v="), + "should use per-module static bundle path" + ); + assert_sha256_hex_hash(hash_query_value(&src)); + } + + #[test] + fn tsjs_deferred_script_src_uses_empty_hash_for_unknown_module() { + assert_eq!( + tsjs_deferred_script_src("unknown-module"), + "/static/tsjs=tsjs-unknown-module.min.js?v=", + "should document current unknown-module hash behavior" + ); + } + + #[test] + fn tsjs_deferred_script_tag_marks_script_defer() { + let src = tsjs_deferred_script_src("prebid"); + + assert_eq!( + tsjs_deferred_script_tag("prebid"), + format!(""), + "should generate a deferred script tag" + ); + } + + #[test] + fn tsjs_deferred_script_tags_returns_empty_for_empty_input() { + assert_eq!( + tsjs_deferred_script_tags(&[]), + "", + "should not emit tags when no deferred modules exist" + ); + } + + #[test] + fn tsjs_deferred_script_tags_preserves_input_order() { + assert_eq!( + tsjs_deferred_script_tags(&["prebid", "creative"]), + format!( + "{}{}", + tsjs_deferred_script_tag("prebid"), + tsjs_deferred_script_tag("creative") + ), + "should preserve caller-provided deferred module order" + ); + } +} From 2a3edb77a3ef376a561ec2a32dafe1e97e34f704 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 13 May 2026 12:56:16 -0500 Subject: [PATCH 2/2] Refine production readiness coverage PR consolidation --- .../src/auction/formats.rs | 4 ++-- crates/trusted-server-core/src/error.rs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index a5b08c3d..e9133844 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -359,7 +359,7 @@ mod tests { page_url: Some("https://test-publisher.com".to_string()), }, user: UserInfo { - id: "synthetic-id".to_string(), + id: "ec-id".to_string(), fresh_id: "fresh-id".to_string(), consent: Some(ConsentContext::default()), }, @@ -634,7 +634,7 @@ mod tests { ); assert_eq!( response.get_header_str(HEADER_X_TS_EC), - Some("synthetic-id"), + Some("ec-id"), "should set EC ID header" ); assert_eq!( diff --git a/crates/trusted-server-core/src/error.rs b/crates/trusted-server-core/src/error.rs index 7f64f90f..070ae493 100644 --- a/crates/trusted-server-core/src/error.rs +++ b/crates/trusted-server-core/src/error.rs @@ -203,6 +203,26 @@ mod tests { #[test] fn status_code_maps_each_error_variant_to_expected_http_response() { + // Compile-time guard: adding a TrustedServerError variant without + // updating this test will fail to compile. + let _exhaustive = |error: &TrustedServerError| match error { + TrustedServerError::BadRequest { .. } + | TrustedServerError::Configuration { .. } + | TrustedServerError::Auction { .. } + | TrustedServerError::Gam { .. } + | TrustedServerError::GdprConsent { .. } + | TrustedServerError::InvalidUtf8 { .. } + | TrustedServerError::InvalidHeaderValue { .. } + | TrustedServerError::KvStore { .. } + | TrustedServerError::Prebid { .. } + | TrustedServerError::Integration { .. } + | TrustedServerError::Proxy { .. } + | TrustedServerError::Forbidden { .. } + | TrustedServerError::AllowlistViolation { .. } + | TrustedServerError::Settings { .. } + | TrustedServerError::Ec { .. } => (), + }; + let cases = vec![ ( TrustedServerError::BadRequest {