You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The deprecated _prepare_gateway_for_read() method mutates the SQLAlchemy ORM object's auth_value field in-place (dict → encoded string). When called on an object still attached to an active session, SQLAlchemy tracks the mutation and writes the encoded string back to the DbGateway.auth_value JSON column on commit — replacing the intended dict with a JSON string.
This is called on attached ORM objects in active sessions across 6 call sites. The safe replacement convert_gateway_to_read() (line 3970) uses __dict__.copy() and does NOT mutate the original, but only 1 of 7 call sites has been migrated.
Affected Call Sites
Line
Method
Risk
Reason
1184
register_gateway()
Medium
Inside session; implicit commit may persist mutation
1730
list_gateways_for_user()
Low
Called after db.commit(), objects may be detached
2271
update_gateway()
High
db.refresh() re-attaches object before mutation
2436
get_gateway()
Medium
Active session, mutation tracked
2727
set_gateway_state()
Medium
Active session, mutation tracked
3726
get_first_gateway_by_url()
Medium
Active session, mutation tracked
The safe convert_gateway_to_read() is used only at line 1623 (list_gateways()).
Impact
When the mutation persists, the JSON column stores a JSON string "encoded_value" instead of a JSON object {"key": "value"}. On subsequent reads:
isinstance(gateway.auth_value, str) → True (no longer a dict)
decode_auth(string) works correctly on the encoded value, so no data loss occurs
However, the column type annotation Mapped[Optional[Dict[str, str]]] is violated — the column holds a string, not a dict
The practical impact is that the fix from #3510 (storing dict in JSON column) gets partially undone by the mutation after each read that goes through _prepare_gateway_for_read.
Suggested Fix
Replace all 6 remaining call sites of _prepare_gateway_for_read() with convert_gateway_to_read():
# BEFORE (mutates ORM object):returnGatewayRead.model_validate(self._prepare_gateway_for_read(gateway)).masked()
# AFTER (safe, uses dict copy):returnself.convert_gateway_to_read(gateway)
convert_gateway_to_read() already handles dict encoding, tag conversion, metadata fields, and returns .masked(). It is a drop-in replacement.
After migration, _prepare_gateway_for_read() can be removed entirely.
Reproduction
Register a gateway with auth_type=authheaders
Query the DB: SELECT auth_value FROM gateways WHERE name='...' → shows JSON object
Call GET /gateways/{id} (triggers _prepare_gateway_for_read)
Query DB again → value may now be a JSON string (encoded) instead of a JSON object
Bug Description
The deprecated
_prepare_gateway_for_read()method mutates the SQLAlchemy ORM object'sauth_valuefield in-place (dict → encoded string). When called on an object still attached to an active session, SQLAlchemy tracks the mutation and writes the encoded string back to theDbGateway.auth_valueJSON column on commit — replacing the intended dict with a JSON string.Root Cause
gateway_service.py:4020-4022:This is called on attached ORM objects in active sessions across 6 call sites. The safe replacement
convert_gateway_to_read()(line 3970) uses__dict__.copy()and does NOT mutate the original, but only 1 of 7 call sites has been migrated.Affected Call Sites
register_gateway()list_gateways_for_user()db.commit(), objects may be detachedupdate_gateway()db.refresh()re-attaches object before mutationget_gateway()set_gateway_state()get_first_gateway_by_url()The safe
convert_gateway_to_read()is used only at line 1623 (list_gateways()).Impact
When the mutation persists, the JSON column stores a JSON string
"encoded_value"instead of a JSON object{"key": "value"}. On subsequent reads:isinstance(gateway.auth_value, str)→ True (no longer a dict)isinstance(auth_value, dict)guards (from PR [FIX][API]: store authheadersauth_valueas dict to prevent JSON null on persist #3510) skip encoding — the value passes through as-isdecode_auth(string)works correctly on the encoded value, so no data loss occursMapped[Optional[Dict[str, str]]]is violated — the column holds a string, not a dictThe practical impact is that the fix from #3510 (storing dict in JSON column) gets partially undone by the mutation after each read that goes through
_prepare_gateway_for_read.Suggested Fix
Replace all 6 remaining call sites of
_prepare_gateway_for_read()withconvert_gateway_to_read():convert_gateway_to_read()already handles dict encoding, tag conversion, metadata fields, and returns.masked(). It is a drop-in replacement.After migration,
_prepare_gateway_for_read()can be removed entirely.Reproduction
auth_type=authheadersSELECT auth_value FROM gateways WHERE name='...'→ shows JSON objectGET /gateways/{id}(triggers_prepare_gateway_for_read)Related
auth_valueas dict to prevent JSON null on persist #3510 — fixes the creation path to store dict, but the mutation partially undoes it_prepare_gateway_for_readis already markedDEPRECATEDin its docstring (line 4007)