This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 191feea3 fix(rest-server): use Java's uppercase resourceType in error
responses (#830)
191feea3 is described below
commit 191feea348c44765f2cdb411d764304e2be93aea
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 17 16:07:58 2026 +0800
fix(rest-server): use Java's uppercase resourceType in error responses
(#830)
---
crates/paimon-rest-server/src/lib.rs | 92 ++++++++++++++++++++++++++++++++++--
1 file changed, 87 insertions(+), 5 deletions(-)
diff --git a/crates/paimon-rest-server/src/lib.rs
b/crates/paimon-rest-server/src/lib.rs
index 60aca42d..7d2a0959 100644
--- a/crates/paimon-rest-server/src/lib.rs
+++ b/crates/paimon-rest-server/src/lib.rs
@@ -194,35 +194,50 @@ fn build_router(prefix: &str, state: Arc<AppState>) ->
Router {
// The client reconstructs the original error solely from the HTTP status code
// (see `crates/paimon/src/api/rest_error.rs`), so the code below MUST line the
// status codes up with `RestError::from_error_response`.
+//
+// `resourceType` is part of the same contract for a *Java* client, which
+// dispatches on it: `RESTCatalog#alterTable` only raises
+// `TableNotExistException` when it equals `ErrorResponse.RESOURCE_TYPE_TABLE`,
+// and `StringUtils#equals` compares char by char, so the case has to match.
// ============================================================================
+/// Mirrors Java `ErrorResponse.RESOURCE_TYPE_DATABASE`.
+const RESOURCE_TYPE_DATABASE: &str = "DATABASE";
+/// Mirrors Java `ErrorResponse.RESOURCE_TYPE_TABLE`.
+const RESOURCE_TYPE_TABLE: &str = "TABLE";
+
fn error_response(e: Error) -> Response {
let (status, resource_type, resource_name) = match &e {
Error::DatabaseNotExist { database } => (
StatusCode::NOT_FOUND,
- Some("database".to_string()),
+ Some(RESOURCE_TYPE_DATABASE.to_string()),
Some(database.clone()),
),
Error::TableNotExist { full_name } => (
StatusCode::NOT_FOUND,
- Some("table".to_string()),
+ Some(RESOURCE_TYPE_TABLE.to_string()),
Some(full_name.clone()),
),
Error::DatabaseAlreadyExist { database } => (
StatusCode::CONFLICT,
- Some("database".to_string()),
+ Some(RESOURCE_TYPE_DATABASE.to_string()),
Some(database.clone()),
),
Error::TableAlreadyExist { full_name } => (
StatusCode::CONFLICT,
- Some("table".to_string()),
+ Some(RESOURCE_TYPE_TABLE.to_string()),
Some(full_name.clone()),
),
Error::DatabaseNotEmpty { database } => (
StatusCode::CONFLICT,
- Some("database".to_string()),
+ Some(RESOURCE_TYPE_DATABASE.to_string()),
Some(database.clone()),
),
+ // `column:<table>` and 400 diverge from Java's `RESOURCE_TYPE_COLUMN`
+ // and 404/409, but correcting the status here would make the *Rust*
+ // client report a missing column as `TableNotExist`
+ // (`map_rest_error_for_table` ignores `resourceType`), so the two must
+ // move together in a separate change.
Error::ColumnNotExist { full_name, column } => (
StatusCode::BAD_REQUEST,
Some(format!("column:{full_name}")),
@@ -608,3 +623,70 @@ async fn table_token_stub() -> Response {
fn empty_audit() -> AuditRESTResponse {
AuditRESTResponse::new(None, None, None, None, None)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ async fn resource_type_of(error: Error) -> (StatusCode, Option<String>) {
+ let response = error_response(error);
+ let status = response.status();
+ let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
+ .await
+ .expect("read body");
+ let json: serde_json::Value =
serde_json::from_slice(&bytes).expect("parse body");
+ let resource_type = json
+ .get("resourceType")
+ .and_then(|value| value.as_str())
+ .map(str::to_string);
+ (status, resource_type)
+ }
+
+ /// A Java client keys off `resourceType` and compares it
case-sensitively, so
+ /// these are wire values, not free-form labels.
+ #[tokio::test]
+ async fn database_and_table_errors_use_javas_uppercase_resource_type() {
+ for (error, expected_status, expected_type) in [
+ (
+ Error::DatabaseNotExist {
+ database: "db".to_string(),
+ },
+ StatusCode::NOT_FOUND,
+ "DATABASE",
+ ),
+ (
+ Error::TableNotExist {
+ full_name: "db.t".to_string(),
+ },
+ StatusCode::NOT_FOUND,
+ "TABLE",
+ ),
+ (
+ Error::DatabaseAlreadyExist {
+ database: "db".to_string(),
+ },
+ StatusCode::CONFLICT,
+ "DATABASE",
+ ),
+ (
+ Error::TableAlreadyExist {
+ full_name: "db.t".to_string(),
+ },
+ StatusCode::CONFLICT,
+ "TABLE",
+ ),
+ (
+ Error::DatabaseNotEmpty {
+ database: "db".to_string(),
+ },
+ StatusCode::CONFLICT,
+ "DATABASE",
+ ),
+ ] {
+ let label = expected_type;
+ let (status, resource_type) = resource_type_of(error).await;
+ assert_eq!(status, expected_status, "{label}");
+ assert_eq!(resource_type.as_deref(), Some(expected_type),
"{label}");
+ }
+ }
+}