eureka-state: an encode failure is not retryable, and a non-UTF-8 endpoint is pinned
A request that fails to encode is a defect in this client, not a transport failure, so it is ClientError::Unencodable, typed Unencodable at the tool, instead of the retryable Unavailable. A test reports a non-UTF-8 HUGINN_ENDPOINT as such. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d9965983f
commit
cd3dfc0268
4 changed files with 28 additions and 6 deletions
|
|
@ -45,13 +45,15 @@ pub const MAX_REQUEST_BYTES: usize = 65_507 - 36 - "schema".len();
|
|||
/// the only one worth retrying: the daemon could not be reached, went quiet
|
||||
/// past the call's deadline, or answered out of protocol. `Rejected` is the
|
||||
/// daemon refusing the envelope itself, and `TooLarge` is the client refusing
|
||||
/// a request that no single send can carry, before sending it; a retry of the
|
||||
/// same call cannot change either.
|
||||
/// a request that no single send can carry, before sending it; `Unencodable` is
|
||||
/// the request failing to encode at all, a defect in this client. A retry of the
|
||||
/// same call cannot change any of those three.
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
Unavailable { endpoint: SocketAddr, detail: String },
|
||||
Rejected { endpoint: SocketAddr, code: String, detail: String },
|
||||
TooLarge { bytes: usize, limit: usize },
|
||||
Unencodable { detail: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for ClientError {
|
||||
|
|
@ -66,6 +68,7 @@ impl fmt::Display for ClientError {
|
|||
Self::TooLarge { bytes, limit } => {
|
||||
write!(f, "the request encodes to {bytes} bytes and one send carries at most {limit}")
|
||||
}
|
||||
Self::Unencodable { detail } => write!(f, "the request could not be encoded: {detail}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,9 +112,10 @@ impl HuginnClient {
|
|||
let deadline = Instant::now() + self.timeout;
|
||||
let unavailable =
|
||||
|error: anyhow::Error| ClientError::Unavailable { endpoint: self.endpoint, detail: format!("{error:#}") };
|
||||
let message = encode_request(MESSAGE_ID, &request, None).map_err(unavailable)?;
|
||||
let unencodable = |error: anyhow::Error| ClientError::Unencodable { detail: format!("{error:#}") };
|
||||
let message = encode_request(MESSAGE_ID, &request, None).map_err(unencodable)?;
|
||||
let bytes = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)
|
||||
.map_err(unavailable)?
|
||||
.map_err(unencodable)?
|
||||
.len();
|
||||
if bytes > MAX_REQUEST_BYTES {
|
||||
return Err(ClientError::TooLarge { bytes, limit: MAX_REQUEST_BYTES });
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ impl Trouble {
|
|||
Self::Client(ClientError::TooLarge { bytes, limit }) => {
|
||||
json!({ "error": "TooLarge", "bytes": bytes, "limit": limit })
|
||||
}
|
||||
Self::Client(ClientError::Unencodable { detail }) => json!({ "error": "Unencodable", "detail": detail }),
|
||||
Self::Internal(detail) => json!({ "error": "Internal", "detail": detail }),
|
||||
}
|
||||
}
|
||||
|
|
@ -317,6 +318,10 @@ mod tests {
|
|||
Trouble::Client(ClientError::TooLarge { bytes: 70_000, limit: 65_000 }).body(),
|
||||
json!({ "error": "TooLarge", "bytes": 70_000, "limit": 65_000 })
|
||||
);
|
||||
assert_eq!(
|
||||
Trouble::Client(ClientError::Unencodable { detail: "no".into() }).body(),
|
||||
json!({ "error": "Unencodable", "detail": "no" })
|
||||
);
|
||||
assert_eq!(Trouble::Misconfigured("bad".into()).body(), json!({ "error": "Misconfigured", "detail": "bad" }));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,14 +128,14 @@ fn client(addr: SocketAddr) -> HuginnClient {
|
|||
fn unavailable(error: ClientError) -> (SocketAddr, String) {
|
||||
match error {
|
||||
ClientError::Unavailable { endpoint, detail } => (endpoint, detail),
|
||||
other @ (ClientError::Rejected { .. } | ClientError::TooLarge { .. }) => panic!("expected Unavailable, got {other}"),
|
||||
other @ (ClientError::Rejected { .. } | ClientError::TooLarge { .. } | ClientError::Unencodable { .. }) => panic!("expected Unavailable, got {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rejected(error: ClientError) -> (SocketAddr, String, String) {
|
||||
match error {
|
||||
ClientError::Rejected { endpoint, code, detail } => (endpoint, code, detail),
|
||||
other @ (ClientError::Unavailable { .. } | ClientError::TooLarge { .. }) => panic!("expected Rejected, got {other}"),
|
||||
other @ (ClientError::Unavailable { .. } | ClientError::TooLarge { .. } | ClientError::Unencodable { .. }) => panic!("expected Rejected, got {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -495,6 +495,19 @@ fn a_non_utf8_instance_is_reported_as_such() {
|
|||
assert_eq!(whoami["instance"], Value::Null);
|
||||
}
|
||||
|
||||
/// Likewise an endpoint that is not UTF-8: reported as that, not as unset.
|
||||
#[test]
|
||||
fn a_non_utf8_endpoint_is_reported_as_such() {
|
||||
let mut mcp = Mcp::spawn(|command| {
|
||||
command.env("EUREKA_INSTANCE", INSTANCE).env("HUGINN_ENDPOINT", non_utf8());
|
||||
});
|
||||
let (error, whoami) = mcp.call("whoami", json!({}));
|
||||
assert!(!error);
|
||||
let detail = whoami["error"]["detail"].as_str().unwrap();
|
||||
assert!(detail.contains("HUGINN_ENDPOINT is not valid UTF-8"), "{whoami}");
|
||||
assert_eq!(whoami["endpoint"], Value::Null);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn non_utf8() -> std::ffi::OsString {
|
||||
std::os::unix::ffi::OsStringExt::from_vec(vec![b'e', 0xff])
|
||||
|
|
|
|||
Loading…
Reference in a new issue