Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Make _get_error_code resilient to malformed API responses#392

Open
gistrec wants to merge 3 commits into
N4S4:masterfrom
gistrec:fix-get-error-code-defensive
Open

Make _get_error_code resilient to malformed API responses#392
gistrec wants to merge 3 commits into
N4S4:masterfrom
gistrec:fix-get-error-code-defensive

Conversation

@gistrec
Copy link
Copy Markdown
Contributor

@gistrec gistrec commented May 8, 2026

Summary

Authentication._get_error_code extracts the error code from a Synology API response by chaining .get() calls:

if response.get("success"):
    code = CODE_SUCCESS
else:
    code = response.get("error").get("code")
return code

If the server returns {"success": false} without an error field — or with error set to None, a non-dict value, or a dict missing code — the second .get() raises AttributeError.

Because this method is called on every API response, a malformed reply turns a recoverable error into an exception with no useful context.

Reproduction

Authentication._get_error_code({"success": False})
# AttributeError: 'NoneType' object has no attribute 'get'

Authentication._get_error_code({"success": False, "error": None})
# AttributeError: 'NoneType' object has no attribute 'get'

Authentication._get_error_code({"success": False, "error": {}})
# returns None instead of an int

The last case is also incorrect because the docstring says the method returns an int.

Fix

Validate the response structure before dereferencing it, and fall back to the existing CODE_UNKNOWN value when the response does not match the documented schema.

CODE_UNKNOWN is already mapped to "Unknown Error" in error_codes.

if response.get("success"):
    return CODE_SUCCESS

error = response.get("error")
if isinstance(error, dict) and isinstance(error.get("code"), int):
    return error["code"]

return CODE_UNKNOWN

Behavior

Input Before After
{"success": True} 0 0
{"success": False, "error": {"code": 100}} 100 100
{"success": False} AttributeError 9999 (CODE_UNKNOWN)
{"success": False, "error": None} AttributeError 9999
{"success": False, "error": {}} None 9999
{"success": False, "error": "oops"} AttributeError 9999
{"success": False, "error": {"code": "NaN"}} "NaN" 9999
{} AttributeError 9999

Callers using if not error_code continue to take the error path. _get_error_message(9999, ...) already returns "Error 9999 - Unknown Error".

Tests

Adds tests/test_auth.py with 11 cases covering:

  • successful responses;
  • normal error responses;
  • missing error;
  • error set to None;
  • error set to {};
  • error set to a non-dict value;
  • error.code set to a non-int value;
  • empty response objects.

All malformed-response cases now return CODE_UNKNOWN instead of raising an exception or returning a non-int value.

Copy link
Copy Markdown
Collaborator

@joeperpetua joeperpetua left a comment

Choose a reason for hiding this comment

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

As you are refactoring this code, it may be nice to also support the extraction of API specific error codes when available instead of just getting the generic one.

For example:

{
  "success":false,

  "error":{

    "code":1100,

    "errors":[{

      "code":408,

      "path":"/test/:"

    }]

  }

}

https://kb.synology.com/en-global/DG/DSM_Login_Web_API_Guide/2

Synology APIs (File Station, Active Directory, etc.) can return a list
of per-item errors inside response['error']['errors'], each with its own
code plus path or msg. Expose them through a defensive accessor that
returns [] on missing or malformed input, matching the style of the
_get_error_code fix.

Addresses review feedback on N4S4#392.
@gistrec
Copy link
Copy Markdown
Contributor Author

gistrec commented May 19, 2026

Thanks for the pointer. Added Authentication._get_error_details (2f2d96e) — returns the entries from response['error']['errors'] (the {code, path} / {code, msg} shape).

Kept as a separate accessor so existing callers and the per-API dispatch in exceptions.py keep receiving the outer code (changing _get_error_code would feed inner codes like 408 into the wrong lookup table). Tests for both File Station and Active Directory shapes are in tests/test_auth.py.

Pre-commit's autopep8 hook flagged several long lines (>79 chars) in
the assertEqual calls. Reformat using local `response`/`expected`
variables (matching the existing style used in
test_normal_error_returns_code_with_extras and test_file_station_shape)
instead of accepting autopep8's mid-call line wraps, which are harder
to read.

No behavior change; same 22 tests still pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants