Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server to client error messages #1406

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion clients/typescript/src/satellite/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,11 @@ export class SatelliteProcess implements Satellite {
throw satelliteError
}

Log.warn(`an error occurred in satellite: ${satelliteError.message}`)
Log.warn(
`an error occurred in satellite: ${
SatelliteErrorCode[satelliteError.code]
} ${satelliteError.message}`
)

this._handleOrThrowClientError(satelliteError)
}
Expand Down
2 changes: 1 addition & 1 deletion clients/typescript/src/util/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function serverErrorToSatelliteError(
): SatelliteError {
return new SatelliteError(
serverErrorToSatError[error.errorType],
'server error'
'Server error: ' + error.message
)
}

Expand Down
6 changes: 5 additions & 1 deletion components/electric/lib/electric/satellite/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Electric.Satellite.Protocol do
alias Electric.Replication.Shapes
alias Electric.Replication.Shapes.ShapeRequest
alias Electric.Satellite.Serialization
alias Electric.Satellite.Serialization.DataValidationError
alias Electric.Satellite.ClientManager
alias Electric.Satellite.WriteValidation
alias Electric.Satellite.ClientReconnectionInfo
Expand Down Expand Up @@ -485,7 +486,7 @@ defmodule Electric.Satellite.Protocol do
Telemetry.event(state, :bad_transaction)

Logger.error(Exception.format(:error, e, __STACKTRACE__))
{:error, %SatErrorResp{error_type: :INVALID_REQUEST}}
{:error, %SatErrorResp{error_type: :INVALID_REQUEST, message: friendly_error_message(e)}}
end
end

Expand Down Expand Up @@ -1350,4 +1351,7 @@ defmodule Electric.Satellite.Protocol do
do: Electric.Postgres.Dialect.SQLite

defp decode_sql_dialect(:POSTGRES), do: Electric.Postgres.Dialect.Postgresql

defp friendly_error_message(%DataValidationError{} = error), do: Exception.message(error)
defp friendly_error_message(_), do: "unexpected error"
end
96 changes: 76 additions & 20 deletions components/electric/lib/electric/satellite/serialization.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Electric.Satellite.Serialization do
alias Electric.Satellite.Protocol
alias Electric.Satellite.SatOpGone
alias Electric.Satellite.Serialization.DataValidationError
alias Electric.Replication.Changes.Gone
alias Electric.Postgres.Extension.SchemaCache
alias Electric.Postgres.{Extension, Replication}
Expand Down Expand Up @@ -559,7 +560,7 @@ defmodule Electric.Satellite.Serialization do
def decode_column_value!(val, :bool) when val in ["t", "f"], do: val

def decode_column_value!(val, :bool) do
raise "Unexpected value for bool column: #{inspect(val)}"
raise DataValidationError, message: "Unexpected value for bool column: #{inspect(val)}"
end

def decode_column_value!(val, type) when type in [:text, :varchar] do
Expand Down Expand Up @@ -589,6 +590,11 @@ defmodule Electric.Satellite.Serialization do
_ = Date.from_iso8601!(val)

val
rescue
_ ->
reraise DataValidationError,
[message: "Unexpected value for :date column: #{inspect(val)}"],
__STACKTRACE__
end

def decode_column_value!(val, type) when type in [:float4, :float8] do
Expand All @@ -599,16 +605,25 @@ defmodule Electric.Satellite.Serialization do
end

def decode_column_value!(val, type) when type in [:int2, :int4, :int8] do
val
|> String.to_integer()
|> assert_valid_integer!(type)
case Integer.parse(val) do
{int, ""} ->
assert_valid_integer!(int, type)
val

val
_ ->
raise DataValidationError, message: "Non-integer value given for #{type} column: #{val}"
end
end

def decode_column_value!(val, type) when type in [:json, :jsonb] do
_ = Jason.decode!(val)
val
case Jason.decode(val) do
{:ok, _} ->
val

{:error, %Jason.DecodeError{} = error} ->
raise DataValidationError,
message: "Invalid JSON found in #{type} column: #{Exception.message(error)}"
end
end

def decode_column_value!(val, :time) do
Expand All @@ -628,36 +643,71 @@ defmodule Electric.Satellite.Serialization do
_ = Time.from_iso8601!(val)

val
rescue
_ ->
reraise DataValidationError,
[message: "Unexpected value for :time column: #{inspect(val)}"],
__STACKTRACE__
end

def decode_column_value!(val, :timestamp) do
# NaiveDateTime silently discards time zone offset if it is present in the string. But we want to reject such strings
# because values of type `timestamp` must not have an offset.
{:error, :missing_offset} = DateTime.from_iso8601(val)
case DateTime.from_iso8601(val) do
{:ok, _, _} ->
# NaiveDateTime silently discards time zone offset if it is present in the string. But we want to reject such strings
# because values of type `timestamp` must not have an offset.
raise DataValidationError,
message: "Unexpected offset provided for timestamp column: #{inspect(val)}"

dt = NaiveDateTime.from_iso8601!(val)
assert_valid_year!(dt.year)
{:error, :missing_offset} ->
dt = NaiveDateTime.from_iso8601!(val)
assert_valid_year!(dt.year)

val
val

{:error, :invalid_format} ->
raise DataValidationError,
message: "Unexpected format provided for timestamp column: #{inspect(val)}"
end
end

def decode_column_value!(val, :timestamptz) do
# The offset of datetimes coming over the Satellite protocol MUST be 0.
{:ok, dt, 0} = DateTime.from_iso8601(val)
assert_valid_year!(dt.year)
case DateTime.from_iso8601(val) do
{:ok, dt, 0} ->
assert_valid_year!(dt.year)

val
val

{:ok, _, _} ->
raise DataValidationError,
message: "Non-zeo offset provided for timestamptz column: #{inspect(val)}"

{:error, :invalid_format} ->
raise DataValidationError,
message: "Unexpected format for timestamptz column: #{inspect(val)}"

{:error, :missing_offset} ->
raise DataValidationError,
message: "Missing offset in timestamptz column: #{inspect(val)}"
end
end

def decode_column_value!(val, :uuid) do
Electric.Utils.validate_uuid!(val)
case Electric.Utils.validate_uuid(val) do
{:ok, uuid} ->
uuid

:error ->
raise DataValidationError, message: "Unexpected value for uuid column: #{inspect(val)}"
end
end

def decode_column_value!(val, {:enum, typename, values}) do
if val in values do
val
else
raise "Unexpected value #{inspect(val)} for enum type #{typename}"
raise DataValidationError,
message: "Unexpected value #{inspect(val)} for enum type #{typename}"
end
end

Expand All @@ -668,7 +718,7 @@ defmodule Electric.Satellite.Serialization do
val

_ ->
raise "Unexpected value for #{type} colum: #{inspect(val)}"
raise DataValidationError, message: "Unexpected value for #{type} column: #{inspect(val)}"
end
end

Expand All @@ -680,6 +730,9 @@ defmodule Electric.Satellite.Serialization do
defp assert_valid_integer!(int, :int4) when int in @int4_range, do: :ok
defp assert_valid_integer!(int, :int8) when int in @int8_range, do: :ok

defp assert_valid_integer!(int, type),
do: raise(DataValidationError, message: "Value out of range for #{type} column: #{int}")

# Postgres[1] uses BC/AD suffixes to indicate whether the date is in the Common Era or precedes it. Postgres assumes year
# 0 did not exist, so in its worldview '0001-12-31 BC' is immediately followed by '0001-01-01'.
#
Expand All @@ -694,6 +747,9 @@ defmodule Electric.Satellite.Serialization do
# [2]: https://www.sqlite.org/lang_datefunc.html
defp assert_valid_year!(year) when year in 1..9999, do: :ok

defp assert_valid_year!(year),
do: raise(DataValidationError, message: "Year out of range: #{year}")

defp assert_valid_month!(month) when month in 1..12, do: :ok

defp assert_valid_day!(day) when day in 1..31, do: :ok
Expand Down Expand Up @@ -731,7 +787,7 @@ defmodule Electric.Satellite.Serialization do
end

with :error <- conversion_result do
raise "Value for float4 column out of range: #{inspect(num)}"
raise DataValidationError, message: "Value for float4 column out of range: #{inspect(num)}"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Electric.Satellite.Serialization.DataValidationError do
defexception [:message]
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Electric.Satellite.SerializationTest do
alias Electric.Postgres.{Lsn, Schema, Extension.SchemaCache}
alias Electric.Replication.Changes.Transaction
alias Electric.Satellite.Serialization
alias Electric.Satellite.Serialization.DataValidationError
alias Electric.Postgres.MockSchemaLoader

describe "map_to_row" do
Expand Down Expand Up @@ -262,7 +263,7 @@ defmodule Electric.Satellite.SerializationTest do
try do
Serialization.decode_record!(row, columns)
rescue
_ -> :ok
DataValidationError -> :ok
else
val -> flunk("Expected decode_record!() to raise but it returned #{inspect(val)}")
end
Expand Down
Loading