How the `rejectReadOnly` DSN Parameter Prevents Accidental Writes to Read‑Only Replicas
The rejectReadOnly DSN parameter detects specific MySQL error codes indicating a read‑only connection, immediately closes the offending connection to signal driver.ErrBadConn, and triggers the Go database/sql pool to retry on a writable primary, eliminating silent write failures in replica environments.
The go-sql-driver/mysql package provides a critical safety mechanism for applications running against MySQL read‑only replicas or Aurora clusters. When enabled, the rejectReadOnly parameter acts as a fail‑fast switch that automatically handles fail‑over scenarios by detecting read‑only session states and forcing connection recycling. This prevents applications from unintentionally continuing with stale read‑only connections that would reject subsequent write operations.
How rejectReadOnly Works
The implementation follows a three‑stage pipeline: configuration parsing, error code detection, and forced connection recycling.
DSN Configuration in dsn.go
When the DSN string contains rejectReadOnly=true, the parser in dsn.go (line 73) populates the Config.RejectReadOnly boolean field. This configuration flag remains active for the lifetime of the connection pool, instructing the driver to monitor for read‑only error conditions on every query execution.
Error Code Detection in packets.go
After each statement execution, the driver examines MySQL error packets in packets.go (lines 998‑1014). The implementation specifically watches for three error codes that indicate a read‑only session:
- 1792 –
ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION - 1290 –
ER_OPTION_PREVENTS_STATEMENT(common during Aurora fail‑over) - 1836 –
ER_READ_ONLY_MODE
If any of these codes are received and RejectReadOnly is enabled, the driver executes mc.Close() to terminate the connection immediately, then returns driver.ErrBadConn instead of propagating the raw MySQL error.
Automatic Connection Recycling
Returning driver.ErrBadConn signals the Go database/sql package to discard the current connection from the pool and establish a fresh one for the next operation. In typical replica setups with proxy or DNS‑based routing, the new connection resolves to a writable primary, allowing the retried write to succeed without application intervention.
Configuration and DSN Setup
To enable the protection, append rejectReadOnly=true to your DSN string:
// DSN with rejectReadOnly enabled – safe for read‑only replica pools
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?rejectReadOnly=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
// Example: a write that would otherwise hit a read‑only replica
_, err = db.Exec("INSERT INTO orders (id, total) VALUES (1, 42)")
if err != nil {
// With rejectReadOnly the driver will have already closed the read‑only
// connection and retried on a writable one, so this error only occurs on
// genuine failures (e.g., syntax error).
log.Fatal(err)
}
Error Detection Logic in packets.go
The core detection logic resides in the packet handling layer. When the server returns an error packet, the driver inspects the error number before returning it to the caller:
- Compare the MySQL error code against the read‑only set (1792, 1290, 1836).
- Check if
mc.cfg.RejectReadOnlyistrue. - If both conditions match, mark the connection as closed via
mc.Close(). - Return
driver.ErrBadConnto force pool eviction.
This approach ensures that the application never retains a connection in an ambiguous read‑only state.
Practical Implementation Examples
The driver's test suite in driver_test.go (lines 291‑322) demonstrates this recovery mechanism:
runTests(t, dsn+"&rejectReadOnly=true", func(dbt *DBTest) {
dbt.mustExec("CREATE TABLE test (value BOOL)")
// Switch the session to read‑only.
dbt.mustExec("SET SESSION TRANSACTION READ ONLY")
// The following drop will trigger the rejectReadOnly logic,
// causing the driver to close the connection and let database/sql
// obtain a writable one before retrying.
dbt.mustExec("DROP TABLE test")
})
Without rejectReadOnly, the DROP TABLE statement would return an error and leave the connection in a read‑only state. With the flag enabled, the driver automatically recovers by closing the connection and allowing the pool to acquire a writable replacement.
Summary
- The
rejectReadOnlyDSN parameter configures the driver to monitor for specific MySQL read‑only error codes (1792, 1290, and 1836) as implemented inpackets.go. - When detected, the driver calls
mc.Close()and returnsdriver.ErrBadConnto force immediate connection eviction. - This mechanism leverages the standard Go
database/sqlconnection pool behavior to obtain a fresh connection, typically routing to a writable primary in replica environments. - The feature provides automatic, transparent fail‑over for Aurora clusters and other read‑only replica setups without requiring application-level retry logic.
Frequently Asked Questions
What MySQL error codes trigger the rejectReadOnly behavior?
The driver monitors for three specific error codes: 1792 (ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION), 1290 (ER_OPTION_PREVENTS_STATEMENT), and 1836 (ER_READ_ONLY_MODE). These codes indicate that the current session is operating in read‑only mode, typically due to server configuration or Aurora fail‑over events.
How does rejectReadOnly differ from standard error handling?
Without rejectReadOnly, the driver returns the raw MySQL error to the application and retains the connection in the pool, leaving it in a read‑only state for subsequent queries. With rejectReadOnly enabled, the driver proactively closes the connection and returns driver.ErrBadConn, forcing database/sql to discard the connection and establish a new one before retrying the operation.
Is rejectReadOnly necessary for Amazon Aurora MySQL clusters?
Yes. Aurora clusters frequently perform fail‑over operations that can leave existing connections pointing to read‑only replicas. Enabling rejectReadOnly=true ensures that writes encountering the 1290 error code (ER_OPTION_PREVENTS_STATEMENT) automatically trigger connection recycling, allowing the application to seamlessly redirect to the new primary writer without manual intervention.
What happens if rejectReadOnly is false and a write hits a read‑only replica?
The MySQL server returns error 1792 or 1290, which the driver passes directly to the application as a standard error. The connection remains in the pool in a read‑only state, meaning subsequent write attempts on that same connection will continue to fail until the application manually handles the error or the connection is naturally recycled.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →