Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **Database error detail in Proxy's logs**: logged database errors again include the message PostgreSQL returned, not just the error kind. The upgraded PostgreSQL client library stopped appending the underlying cause when an error is rendered as text, which left entries such as `Database connection error` reading only `db error`. Proxy now renders the cause itself, so the server's message is back in the log line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this entry describes a regression that no user saw. The tokio-postgres bump and the ErrorChain fix are both in this PR, so a released Proxy never logged a bare db error. 3.0.1 logs db error: ERROR: … and so does this branch. A reader of the release notes would think that database errors in the logs were broken in a released version.

I suggest that you remove this entry, or merge one sentence into the ### Security entry below. For example: "Logged database errors still include the message from PostgreSQL." Not blocking.


- **Extended-protocol execution lifecycle regressions**: statement duration and slow-statement metrics now describe each execution rather than the lifetime of its cached prepared statement; distinct portals keep isolated Bind measurements; suspended executions retain their metrics until completion; correlated stale responses and inaccessible connection protocol state close the connection instead of silently omitting metadata transitions; uncorrelated responses retain PostgreSQL passthrough behavior; decryption failures no longer report pending schema changes as successful; and disabling mapping no longer creates empty statement metrics.

- **Query cancellation through Proxy**: Cancellation requests now reach the matching PostgreSQL connection, and their routing entries are removed when the client connection exits. Previously cancellation requests arrived on a separate connection that could not find the original route; retaining those routes globally without cleanup could also leak memory and eventually reject a new connection if PostgreSQL reused a cancellation key.
Expand All @@ -30,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Security

- **Updated PostgreSQL client library**: Proxy is now built against `tokio-postgres` 0.7.18, which resolves [GHSA-3gjw-f78c-vvpw](https://github.com/advisories/GHSA-3gjw-f78c-vvpw). No configuration change is required.

- **DDL now updates encryption metadata transactionally**: Proxy applies schema changes only after PostgreSQL confirms execution, keeps successful changes connection-local until commit, and atomically publishes schema and EQL domain metadata before reporting idle readiness. Extended-protocol DDL, explicit transactions, savepoints, rollbacks, one-`Sync` pipelining, and already-open connections now observe the correct schema generation. Unmodelled DDL, simple-query batches whose DDL may change encryption metadata before a dependent statement, and failed catalog publication fail closed instead of risking plaintext writes through stale metadata; encryption-neutral DDL and native temporary-table batches remain compatible.

## [3.0.1] - 2026-08-05
Expand Down
84 changes: 57 additions & 27 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions packages/cipherstash-proxy-integration/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,52 @@ pub fn interleaved_indices(len: usize) -> Vec<usize> {
indices
}

/// Asserts that `result` failed with a PostgreSQL `ErrorResponse` carrying
/// exactly this severity and this message.
///
/// Read the message off the `DbError` rather than off `err.to_string()`.
/// `tokio_postgres::Error`'s `Display` renders only the error *kind* — since
/// 0.7.18 it no longer appends its cause — so `to_string()` yields a bare
/// `"db error"` and pins nothing. The server's text is unchanged and still
/// reachable through `as_db_error()`, which is where `Display` used to read it
/// from, so asserting there keeps the exact customer-visible wording pinned.
///
/// `message` is the primary message field only: the `"db error: "` kind prefix
/// and the `"ERROR: "`/`"FATAL: "` severity prefix that `Display` used to
/// compose are asserted as `severity`, not as part of the text.
pub fn assert_db_error<T>(result: Result<T, tokio_postgres::Error>, severity: &str, message: &str) {
let Err(err) = result else {
panic!("expected a database error, got a successful result");
};

let db_error = err
.as_db_error()
.unwrap_or_else(|| panic!("expected a database error, got: {err:?}"));

assert_eq!(db_error.severity(), severity);
assert_eq!(db_error.message(), message);
}

/// Asserts that `result` failed in the client, before the statement reached the
/// server, with exactly this error kind and this underlying cause.
///
/// The counterpart to [`assert_db_error`] for errors that never become a
/// PostgreSQL `ErrorResponse` — a `ToSql` conversion failure, for example. The
/// kind is what `tokio_postgres::Error` itself renders; the detail is the cause
/// it no longer appends, read back through `source()`.
pub fn assert_client_error<T>(result: Result<T, tokio_postgres::Error>, kind: &str, cause: &str) {
let Err(err) = result else {
panic!("expected a client error, got a successful result");
};

assert_eq!(err.to_string(), kind);

let source = std::error::Error::source(&err)
.unwrap_or_else(|| panic!("expected the error to carry a cause, got: {err:?}"));

assert_eq!(source.to_string(), cause);
}

///
/// Configure the client TLS settings.
/// These are the settings for connecting to the database with TLS.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#[cfg(test)]
mod tests {
use tracing::{debug, info};
use tracing::debug;

use crate::common::{clear, connect_with_tls, random_id, reset_schema, trace, PROXY};
use crate::common::{
assert_client_error, assert_db_error, clear, connect_with_tls, random_id, reset_schema,
trace, PROXY,
};

/// A statement that always fails inside the proxy, at Parse, in every
/// configuration: the proxy's SQL parser rejects it before it reaches the
Expand Down Expand Up @@ -45,14 +48,11 @@ mod tests {
let sql = "INSERT INTO encrypted (id, encrypted_unconfigured) VALUES ($1, $2)";
let result = client.query(sql, &[&id, &encrypted_text]).await;

assert!(result.is_err());

if let Err(err) = result {
let msg = err.to_string();
assert_eq!(msg, "db error: ERROR: column \"encrypted_unconfigured\" of relation \"encrypted\" does not exist");
} else {
unreachable!();
}
assert_db_error(
result,
"ERROR",
"column \"encrypted_unconfigured\" of relation \"encrypted\" does not exist",
);
}

/// A storage-only encrypted column round-trips.
Expand Down Expand Up @@ -110,14 +110,11 @@ mod tests {
let sql = "INSERT INTO encrypted (id, encrypted_date) VALUES ($1, $2)";
let result = client.query(sql, &[&id, &encrypted_date]).await;

assert!(result.is_err());

if let Err(err) = result {
let msg = err.to_string();
assert_eq!(msg, "error serializing parameter 1: cannot convert between the Rust type `i32` and the Postgres type `date`");
} else {
unreachable!();
}
assert_client_error(
result,
"error serializing parameter 1",
"cannot convert between the Rust type `i32` and the Postgres type `date`",
);
}

/// CIP-3678 regression: a statement that fails inside the proxy on a
Expand Down Expand Up @@ -210,14 +207,10 @@ mod tests {
let sql = "INSERT INTO encrypted id, encrypted_text VALUES ($1, $2)";
let result = client.query(sql, &[&id, &encrypted_text]).await;

assert!(result.is_err());

if let Err(err) = result {
let msg = err.to_string();
info!("{}", msg);
assert_eq!(msg, "db error: ERROR: sql parser error: Expected: SELECT, VALUES, or a subquery in the query body, found: id at Line: 1, Column: 23. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#mapping-invalid-sql-statement");
} else {
unreachable!();
}
assert_db_error(
result,
"ERROR",
"sql parser error: Expected: SELECT, VALUES, or a subquery in the query body, found: id at Line: 1, Column: 23. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#mapping-invalid-sql-statement",
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#[cfg(test)]
mod tests {
use crate::common::{
clear, connect_with_tls, random_id, rows_to_vec, simple_query_with_client, trace, PROXY,
assert_db_error, clear, connect_with_tls, random_id, rows_to_vec, simple_query_with_client,
trace, PROXY,
};
use uuid::Uuid;

Expand Down Expand Up @@ -292,15 +293,12 @@ mod tests {

let insert_sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)";
let result = client.query(insert_sql, &[&id, &text]).await;
assert!(result.is_err());

if let Err(err) = result {
let msg = err.to_string();

assert_eq!(msg, "db error: FATAL: Unknown keyset name or id '2cace9db-3a2a-4b46-a184-ba412b3e0730'. Check the configured credentials. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#encrypt-unknown-keyset");
} else {
unreachable!();
}
assert_db_error(
result,
"FATAL",
"Unknown keyset name or id '2cace9db-3a2a-4b46-a184-ba412b3e0730'. Check the configured credentials. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#encrypt-unknown-keyset",
);

// --------
// Switch back to TENANT_1
Expand Down
Loading
Loading