Skip to content

[BUG][API]: _prepare_gateway_for_read mutates ORM object, causing auth_value dict→string writeback #3540

@crivetimihai

Description

@crivetimihai

Bug Description

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.

Root Cause

gateway_service.py:4020-4022:

def _prepare_gateway_for_read(self, gateway: DbGateway) -> DbGateway:
    # ...
    if isinstance(gateway.auth_value, dict):
        gateway.auth_value = encode_auth(gateway.auth_value)  # MUTATES ORM object

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:

  1. isinstance(gateway.auth_value, str) → True (no longer a dict)
  2. Code paths with isinstance(auth_value, dict) guards (from PR [FIX][API]: store authheadersauth_value as dict to prevent JSON null on persist #3510) skip encoding — the value passes through as-is
  3. decode_auth(string) works correctly on the encoded value, so no data loss occurs
  4. 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):
return GatewayRead.model_validate(self._prepare_gateway_for_read(gateway)).masked()

# AFTER (safe, uses dict copy):
return self.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

  1. Register a gateway with auth_type=authheaders
  2. Query the DB: SELECT auth_value FROM gateways WHERE name='...' → shows JSON object
  3. Call GET /gateways/{id} (triggers _prepare_gateway_for_read)
  4. Query DB again → value may now be a JSON string (encoded) instead of a JSON object

Related

Metadata

Metadata

Labels

apiREST API Related itembugSomething isn't workingpythonPython / backend development (FastAPI)triageIssues / Features awaiting triage

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions