///|
/// Errors surfaced by the client.
pub(all) suberror ChromaError {
/// The request could not be produced or sent (bad URL, transport failure).
Transport(String)
/// The server answered with a non-2xx status and a non-JSON body, or one
/// that does not follow Chroma's error envelope.
///
/// Prefer `Api` when the body is JSON: it carries the same information in
/// parsed form. This variant stays for responses the client cannot parse.
Status(Int32, String)
/// The server answered with a non-2xx status and a documented error body.
Api(ApiError)
/// The response body was not the JSON shape this client expects.
Decode(String)
/// The arguments would be rejected by Chroma; nothing was sent.
InvalidArgument(String)
}
///|
/// A non-2xx response, split into the fields Chroma documents.
///
/// Callers get the message without writing a parser or a regular expression:
/// `error` is the short machine-readable label, `message` the human sentence,
/// and `details` whatever extra structure the server attached.
pub(all) struct ApiError {
/// HTTP status code.
status : Int32
/// The `error` field, e.g. `"NotFound"` or `"InternalError"`. Empty when
/// the server omitted it.
error_code : String
/// The `message` field, falling back to `error` when the server sent only
/// a label.
message : String
/// The unparsed body, kept so a caller can still see exactly what arrived.
body : String
/// Extra `details` attached to the error body.
details : Json
} derive(Eq, Debug)
///|
/// Decodes a non-2xx body into an `ApiError`.
///
/// Chroma answers with `{"error": "...", "message": "..."}` on most paths.
/// Both fields are optional in practice: a proxy or an older server may send
/// only one, or neither, so nothing here is treated as a hard decode failure
/// and the raw body is always preserved.
pub fn ApiError::from_json(status : Int32, body : String) -> ApiError {
let parsed = @json.parse(body.trim()) catch { _ => Json::null() }
let fields = match parsed {
Json::Object(fields) => Some(fields)
_ => None
}
let error_code = match fields {
Some(fields) =>
match fields.get("error") {
Some(Json::String(s)) => s
_ => ""
}
None => ""
}
let message = match fields {
Some(fields) =>
match fields.get("message") {
Some(Json::String(s)) => s
_ =>
// Some endpoints put the sentence in `error` and skip `message`.
match fields.get("error") {
Some(Json::String(s)) => s
_ => ""
}
}
None => ""
}
let details = match fields {
Some(fields) =>
match fields.get("details") {
Some(value) => value
None => Json::null()
}
None => Json::null()
}
{ status, error_code, message, body, details, }
}
///|
/// Builds the error to raise for a non-2xx response.
///
/// The body is probed for the Chroma error envelope; only when it carries
/// neither `error` nor `message` does the error stay a raw `Status`.
pub fn api_error(status : Int32, body : String) -> ChromaError {
let parsed = ApiError::from_json(status, body)
if parsed.error_code == "" && parsed.message == "" {
ChromaError::Status(status, body)
} else {
ChromaError::Api(parsed)
}
}
///|
impl Eq for ChromaError with fn equal(self, other) {
match (self, other) {
(Transport(a), Transport(b)) => a == b
(Status(a_code, a_body), Status(b_code, b_body)) =>
a_code == b_code && a_body == b_body
(Api(a), Api(b)) => a == b
(Decode(a), Decode(b)) => a == b
(InvalidArgument(a), InvalidArgument(b)) => a == b
_ => false
}
}
///|
impl Show for ChromaError with fn output(self, logger) {
match self {
Transport(message) => logger.write_string("transport error: " + message)
Status(code, body) =>
logger.write_string(
"server returned status " + code.to_string() + ": " + body,
)
Api(error) => logger.write_string(error.to_string())
Decode(message) => logger.write_string("decode error: " + message)
InvalidArgument(message) =>
logger.write_string("invalid argument: " + message)
}
}
///|
impl Show for ApiError with fn output(self, logger) {
logger.write_string("server returned status " + self.status.to_string())
if self.error_code != "" {
logger.write_string(" (" + self.error_code + ")")
}
if self.message != "" {
logger.write_string(": " + self.message)
} else if self.body.trim() != "" {
logger.write_string(": " + self.body)
}
}
///|
/// Extracts the numeric status of any server-side response error.
///
/// `None` means the error never reached the server, so retrying it is the
/// caller's decision rather than a given.
pub fn ChromaError::status_code(self : ChromaError) -> Int32? {
match self {
Status(code, _) => Some(code)
Api(error) => Some(error.status)
_ => None
}
}
///|
/// Whether the error is worth retrying: 429 and 5xx, i.e. the server was
/// reachable but too busy or broken. Client errors (4xx except 429) are
/// deterministic and repeating them only wastes a round-trip.
pub fn ChromaError::is_retryable(self : ChromaError) -> Bool {
match self.status_code() {
Some(code) => code == 429 || (code >= 500 && code < 600)
None => false
}
}
///|
/// The message a caller can show a user, without the transport wrapper.
pub fn ChromaError::message(self : ChromaError) -> String {
match self {
Transport(text) => text
Status(_, body) => body
Api(error) =>
if error.message != "" {
error.message
} else {
error.error_code
}
Decode(text) => text
InvalidArgument(text) => text
}
}
///|
/// Validates a pagination window.
///
/// Chroma rejects a request where `offset` is not smaller than `limit`,
/// because the two are meant to walk a result set together. Catching it here
/// turns a server round-trip and an opaque 400 into a local, actionable
/// error.
pub fn validate_limit_offset(
limit : Int32?,
offset : Int32?,
) -> Unit raise ChromaError {
let limit = match limit {
Some(value) => value
None => 0
}
let offset = match offset {
Some(value) => value
None => 0
}
if limit < 0 {
raise ChromaError::InvalidArgument("limit must not be negative")
}
if offset < 0 {
raise ChromaError::InvalidArgument("offset must not be negative")
}
if limit != 0 && offset >= limit {
raise ChromaError::InvalidArgument(
"offset (" +
offset.to_string() +
") must be smaller than limit (" +
limit.to_string() +
"); Chroma cannot skip past the end of a page",
)
}
}
///|
pub extend ChromaError with Eq::{not_equal, equal}
///|
pub extend ChromaError with Show::{to_string, output}
///|
pub extend ApiError with Eq::{not_equal, equal}
///|
pub extend ApiError with @debug.Debug::{to_repr}
///|
pub extend ApiError with Show::{to_string, output}