From 4b7f5b613437c2ef46c666882314e2f126cefa10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Tue, 22 Sep 2026 13:57:56 +0300 Subject: [PATCH 1/3] fix: reject dot-segment path parameters `.` and `..` survive percent-encoding as literal dots, so urllib3 normalizes them out of the path before the request leaves. Passing `..` as an ID turned `GET /v4/events/..` into a request for `/v4/`, reaching an endpoint the caller never asked for. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidPathParameterError` before sending, exposing the rejected `parameter` and `value`. It subclasses `ApiValueError`. Related-Task: INTER-2505 --- .../reject-dot-segment-path-parameters.md | 5 + docs/FingerprintApi.md | 15 ++ fingerprint_server_sdk/__init__.py | 2 + fingerprint_server_sdk/api_client.py | 8 +- fingerprint_server_sdk/exceptions.py | 12 ++ template/__init__package.mustache | 1 + template/api_client.mustache | 8 +- template/api_doc.mustache | 7 + template/exceptions.mustache | 12 ++ template/exports_package.mustache | 1 + test/test_path_params.py | 185 ++++++++++++++++++ 11 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 .changeset/reject-dot-segment-path-parameters.md create mode 100644 test/test_path_params.py diff --git a/.changeset/reject-dot-segment-path-parameters.md b/.changeset/reject-dot-segment-path-parameters.md new file mode 100644 index 00000000..1ba73659 --- /dev/null +++ b/.changeset/reject-dot-segment-path-parameters.md @@ -0,0 +1,5 @@ +--- +'@fingerprint/python-sdk': patch +--- + +Reject `.` and `..` as path parameter values. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidPathParameterError` without sending a request. diff --git a/docs/FingerprintApi.md b/docs/FingerprintApi.md index 785d4333..a6bd9766 100644 --- a/docs/FingerprintApi.md +++ b/docs/FingerprintApi.md @@ -86,6 +86,11 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **visitor_id** | **str**| The [visitor ID](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) you want to delete. | +> [!WARNING] +> #### Invalid path parameter values +> +> `visitor_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. + ### Return type void (empty response body) @@ -164,6 +169,11 @@ Name | Type | Description | Notes **event_id** | **str**| The unique [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id) of each identification request (`requestId` can be used in its place). | **ruleset_id** | **str**| The ID of the ruleset to evaluate against the event, producing the action to take for this event. The resulting action is returned in the `rule_action` attribute of the response. | [optional] +> [!WARNING] +> #### Invalid path parameter values +> +> `event_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. + ### Return type [**Event**](Event.md) @@ -461,6 +471,11 @@ Name | Type | Description | Notes **event_id** | **str**| The unique event [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id). | **event_update** | [**EventUpdate**](EventUpdate.md)| | +> [!WARNING] +> #### Invalid path parameter values +> +> `event_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. + ### Return type void (empty response body) diff --git a/fingerprint_server_sdk/__init__.py b/fingerprint_server_sdk/__init__.py index afa630bd..87c2dd50 100644 --- a/fingerprint_server_sdk/__init__.py +++ b/fingerprint_server_sdk/__init__.py @@ -37,6 +37,7 @@ 'TooManyRequestsException', 'ServiceException', 'GatewayTimeoutException', + 'InvalidPathParameterError', 'WebhookValidation', 'DecryptionKey', 'DecryptionAlgorithm', @@ -128,6 +129,7 @@ from fingerprint_server_sdk.exceptions import UnprocessableEntityException from fingerprint_server_sdk.exceptions import TooManyRequestsException from fingerprint_server_sdk.exceptions import GatewayTimeoutException +from fingerprint_server_sdk.exceptions import InvalidPathParameterError # import models into sdk package from fingerprint_server_sdk.models.bot_info import BotInfo diff --git a/fingerprint_server_sdk/api_client.py b/fingerprint_server_sdk/api_client.py index 7469b4e1..d46466d3 100644 --- a/fingerprint_server_sdk/api_client.py +++ b/fingerprint_server_sdk/api_client.py @@ -37,6 +37,7 @@ from fingerprint_server_sdk.exceptions import ( ApiException, ApiValueError, + InvalidPathParameterError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -169,9 +170,10 @@ def param_serialize( ) for k, v in path_params_tuples: # specified safe chars, encode everything - resource_path = resource_path.replace( - '{' + k + '}', quote(str(v), safe=config.safe_chars_for_path_param) - ) + encoded = quote(str(v), safe=config.safe_chars_for_path_param) + if encoded in ('.', '..'): + raise InvalidPathParameterError(k, str(v)) + resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters post_params_result: Any = None diff --git a/fingerprint_server_sdk/exceptions.py b/fingerprint_server_sdk/exceptions.py index 1361a649..7fce8132 100644 --- a/fingerprint_server_sdk/exceptions.py +++ b/fingerprint_server_sdk/exceptions.py @@ -236,6 +236,18 @@ class GatewayTimeoutException(ServiceException): pass +class InvalidPathParameterError(ApiValueError): + """Exception when ``.`` or ``..`` used as resource identifier.""" + + def __init__(self, parameter: str, value: str) -> None: + self.parameter = parameter + self.value = value + super().__init__( + f'invalid value {value!r} for path parameter {parameter}: ' + 'path segment does not identify a resource' + ) + + def render_path(path_to_item: list[Any]) -> str: """Returns a string representation of a path""" result = '' diff --git a/template/__init__package.mustache b/template/__init__package.mustache index 430c1e57..b52f5f66 100644 --- a/template/__init__package.mustache +++ b/template/__init__package.mustache @@ -26,6 +26,7 @@ __all__ = [ "TooManyRequestsException", "ServiceException", "GatewayTimeoutException", + "InvalidPathParameterError", "WebhookValidation", "DecryptionKey", "DecryptionAlgorithm", diff --git a/template/api_client.mustache b/template/api_client.mustache index 04c90402..1c485f73 100644 --- a/template/api_client.mustache +++ b/template/api_client.mustache @@ -28,6 +28,7 @@ from {{packageName}}.configuration import Configuration from {{packageName}}.exceptions import ( ApiException, ApiValueError, + InvalidPathParameterError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -176,9 +177,10 @@ class ApiClient: ) for k, v in path_params_tuples: # specified safe chars, encode everything - resource_path = resource_path.replace( - '{' + k + '}', quote(str(v), safe=config.safe_chars_for_path_param) - ) + encoded = quote(str(v), safe=config.safe_chars_for_path_param) + if encoded in ('.', '..'): + raise InvalidPathParameterError(k, str(v)) + resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters post_params_result: Any = None diff --git a/template/api_doc.mustache b/template/api_doc.mustache index 3f3d9dd2..b31c7b6a 100644 --- a/template/api_doc.mustache +++ b/template/api_doc.mustache @@ -32,6 +32,13 @@ Name | Type | Description | Notes {{#allParams}} **{{paramName}}** | {{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{baseType}}.md){{/isPrimitiveType}}{{/isFile}}| {{{description}}} | {{^required}}[optional] {{/required}}{{#defaultValue}}[default to {{.}}]{{/defaultValue}} {{/allParams}} +{{#pathParams}} +> [!WARNING] +> #### Invalid path parameter values +> +> `{{paramName}}` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. + +{{/pathParams}} ### Return type {{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}void (empty response body){{/returnType}} diff --git a/template/exceptions.mustache b/template/exceptions.mustache index 9431f0c8..aee50af8 100644 --- a/template/exceptions.mustache +++ b/template/exceptions.mustache @@ -222,6 +222,18 @@ class GatewayTimeoutException(ServiceException): pass +class InvalidPathParameterError(ApiValueError): + """Exception when ``.`` or ``..`` used as resource identifier.""" + + def __init__(self, parameter: str, value: str) -> None: + self.parameter = parameter + self.value = value + super().__init__( + "invalid value {0!r} for path parameter {1}: " + "path segment does not identify a resource".format(value, parameter) + ) + + def render_path(path_to_item: list[Any]) -> str: """Returns a string representation of a path""" result = "" diff --git a/template/exports_package.mustache b/template/exports_package.mustache index 33afda40..e0b6818c 100644 --- a/template/exports_package.mustache +++ b/template/exports_package.mustache @@ -20,6 +20,7 @@ from {{packageName}}.exceptions import ConflictException from {{packageName}}.exceptions import UnprocessableEntityException from {{packageName}}.exceptions import TooManyRequestsException from {{packageName}}.exceptions import GatewayTimeoutException +from {{packageName}}.exceptions import InvalidPathParameterError {{#hasHttpSignatureMethods}} from {{packageName}}.signing import HttpSigningConfiguration {{/hasHttpSignatureMethods}} diff --git a/test/test_path_params.py b/test/test_path_params.py new file mode 100644 index 00000000..f9ccc516 --- /dev/null +++ b/test/test_path_params.py @@ -0,0 +1,185 @@ +"""Tests for how path parameter values reach the Server API.""" + +import threading +import unittest +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Callable, Optional + +from fingerprint_server_sdk import ( + Configuration, + EventUpdate, + InvalidPathParameterError, +) +from fingerprint_server_sdk.api.fingerprint_api import FingerprintApi + +API_KEY = '' + +RESPONSE_BODY = b'{"event_id": "1708102555327.NLOjmg", "timestamp": 1708102555327}' + + +@dataclass(frozen=True) +class PathParamOperation: + """An operation that takes an ID as a URL path parameter.""" + + name: str + param: str + prefix: str + call: Callable[[FingerprintApi, str], object] + + +OPERATIONS = ( + PathParamOperation( + name='get_event', + param='event_id', + prefix='/events/', + call=lambda api, event_id: api.get_event(event_id), + ), + PathParamOperation( + name='update_event', + param='event_id', + prefix='/events/', + call=lambda api, event_id: api.update_event(event_id, EventUpdate(suspect=True)), + ), + PathParamOperation( + name='delete_visitor_data', + param='visitor_id', + prefix='/visitors/', + call=lambda api, visitor_id: api.delete_visitor_data(visitor_id), + ), +) + + +class StubServer: + """A local HTTP server recording the request target it was last asked for.""" + + def __init__(self) -> None: + self.request_target: Optional[str] = None + server = self + + class Handler(BaseHTTPRequestHandler): + def _handle(self) -> None: + server.request_target = self.path.split('?', 1)[0] + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(RESPONSE_BODY))) + self.end_headers() + self.wfile.write(RESPONSE_BODY) + + do_GET = _handle + do_PATCH = _handle + do_DELETE = _handle + + def log_message(self, *args: object) -> None: + """Silence the default logging.""" + + self._server = HTTPServer(('127.0.0.1', 0), Handler) + self._thread = threading.Thread(target=self._server.serve_forever, daemon=True) + + def start(self) -> None: + self._thread.start() + + def stop(self) -> None: + self._server.shutdown() + self._server.server_close() + self._thread.join() + + def reset(self) -> None: + self.request_target = None + + @property + def base_path(self) -> str: + host, port = self._server.server_address[:2] + return f'http://{host!s}:{port!s}/base' + + +class TestPathParams(unittest.TestCase): + """Test path parameter handling for every operation that takes an ID in the path.""" + + server: StubServer + + @classmethod + def setUpClass(cls) -> None: + cls.server = StubServer() + cls.server.start() + + @classmethod + def tearDownClass(cls) -> None: + cls.server.stop() + + def setUp(self) -> None: + self.server.reset() + self.api = FingerprintApi(Configuration(api_key=API_KEY, host=self.server.base_path)) + + def call(self, operation: PathParamOperation, value: str) -> Optional[str]: + """Run operation with value and return the request target the server received.""" + self.server.reset() + operation.call(self.api, value) + return self.server.request_target + + def test_value_is_encoded_into_a_single_path_segment(self) -> None: + """A value travels as one opaque segment, so it cannot inject path structure.""" + cases = ( + ('path', '../events', '..%2Fevents'), + ('nested path', '../../base/events', '..%2F..%2Fbase%2Fevents'), + ('leading slash', '/events/123', '%2Fevents%2F123'), + ('absolute url', 'https://test.com/events', 'https%3A%2F%2Ftest.com%2Fevents'), + ('protocol relative url', '//test.com', '%2F%2Ftest.com'), + ('query injection', '123?limit=1', '123%3Flimit%3D1'), + ('fragment injection', '123#fragment', '123%23fragment'), + ('whitespace', 'hello world', 'hello%20world'), + ('non ascii', 'é', '%C3%A9'), + ('pre-encoded', '..%2Fevents', '..%252Fevents'), + ) + + for operation in OPERATIONS: + for name, value, encoded in cases: + with self.subTest(operation=operation.name, case=name): + request_target = self.call(operation, value) + + self.assertEqual(f'/base{operation.prefix}{encoded}', request_target) + + def test_dot_in_value_is_not_touched(self) -> None: + """Test dots in the value stay literal.""" + cases = ( + ('event id', '1708102555327.NLOjmg'), + ('three dots', '...'), + ('leading dot', '.leading'), + ('trailing dot', 'trailing.'), + ) + + for operation in OPERATIONS: + for name, value in cases: + with self.subTest(operation=operation.name, case=name): + request_target = self.call(operation, value) + + self.assertEqual(f'/base{operation.prefix}{value}', request_target) + + def test_dot_segment_is_rejected_without_sending_a_request(self) -> None: + """`.` and `..` are refused""" + for operation in OPERATIONS: + for value in ('.', '..'): + with self.subTest(operation=operation.name, value=value): + self.server.reset() + + with self.assertRaises(InvalidPathParameterError) as context: + operation.call(self.api, value) + + self.assertIsNone(self.server.request_target) + self.assertEqual(operation.param, context.exception.parameter) + self.assertEqual(value, context.exception.value) + self.assertIn(operation.param, str(context.exception)) + + def test_empty_value_does_not_address_the_collection(self) -> None: + """An empty ID leaves a trailing slash""" + for operation in OPERATIONS: + with self.subTest(operation=operation.name): + request_target = self.call(operation, '') + + self.assertEqual(f'/base{operation.prefix}', request_target) + self.assertNotEqual(f'/base{operation.prefix.rstrip("/")}', request_target) + + +if __name__ == '__main__': + unittest.main() From 836247fe29b4e4d0e66288ff5956e24c9ac8c340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Tue, 22 Sep 2026 15:22:06 +0300 Subject: [PATCH 2/3] refactor: rename InvalidPathParameterError to InvalidParameterError --- .changeset/reject-dot-segment-path-parameters.md | 2 +- docs/FingerprintApi.md | 12 ++++++------ fingerprint_server_sdk/__init__.py | 4 ++-- fingerprint_server_sdk/api_client.py | 4 ++-- fingerprint_server_sdk/exceptions.py | 7 ++----- template/__init__package.mustache | 2 +- template/api_client.mustache | 4 ++-- template/api_doc.mustache | 4 ++-- template/exceptions.mustache | 7 ++----- template/exports_package.mustache | 2 +- test/test_path_params.py | 4 ++-- 11 files changed, 23 insertions(+), 29 deletions(-) diff --git a/.changeset/reject-dot-segment-path-parameters.md b/.changeset/reject-dot-segment-path-parameters.md index 1ba73659..875c3936 100644 --- a/.changeset/reject-dot-segment-path-parameters.md +++ b/.changeset/reject-dot-segment-path-parameters.md @@ -2,4 +2,4 @@ '@fingerprint/python-sdk': patch --- -Reject `.` and `..` as path parameter values. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidPathParameterError` without sending a request. +Reject `.` and `..` as event and visitor IDs. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidParameterError` without sending a request. diff --git a/docs/FingerprintApi.md b/docs/FingerprintApi.md index a6bd9766..44de6e97 100644 --- a/docs/FingerprintApi.md +++ b/docs/FingerprintApi.md @@ -87,9 +87,9 @@ Name | Type | Description | Notes **visitor_id** | **str**| The [visitor ID](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) you want to delete. | > [!WARNING] -> #### Invalid path parameter values +> #### Invalid values > -> `visitor_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. +> `visitor_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. ### Return type @@ -170,9 +170,9 @@ Name | Type | Description | Notes **ruleset_id** | **str**| The ID of the ruleset to evaluate against the event, producing the action to take for this event. The resulting action is returned in the `rule_action` attribute of the response. | [optional] > [!WARNING] -> #### Invalid path parameter values +> #### Invalid values > -> `event_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. +> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. ### Return type @@ -472,9 +472,9 @@ Name | Type | Description | Notes **event_update** | [**EventUpdate**](EventUpdate.md)| | > [!WARNING] -> #### Invalid path parameter values +> #### Invalid values > -> `event_id` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. +> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. ### Return type diff --git a/fingerprint_server_sdk/__init__.py b/fingerprint_server_sdk/__init__.py index 87c2dd50..aa31ee70 100644 --- a/fingerprint_server_sdk/__init__.py +++ b/fingerprint_server_sdk/__init__.py @@ -37,7 +37,7 @@ 'TooManyRequestsException', 'ServiceException', 'GatewayTimeoutException', - 'InvalidPathParameterError', + 'InvalidParameterError', 'WebhookValidation', 'DecryptionKey', 'DecryptionAlgorithm', @@ -129,7 +129,7 @@ from fingerprint_server_sdk.exceptions import UnprocessableEntityException from fingerprint_server_sdk.exceptions import TooManyRequestsException from fingerprint_server_sdk.exceptions import GatewayTimeoutException -from fingerprint_server_sdk.exceptions import InvalidPathParameterError +from fingerprint_server_sdk.exceptions import InvalidParameterError # import models into sdk package from fingerprint_server_sdk.models.bot_info import BotInfo diff --git a/fingerprint_server_sdk/api_client.py b/fingerprint_server_sdk/api_client.py index d46466d3..0918c053 100644 --- a/fingerprint_server_sdk/api_client.py +++ b/fingerprint_server_sdk/api_client.py @@ -37,7 +37,7 @@ from fingerprint_server_sdk.exceptions import ( ApiException, ApiValueError, - InvalidPathParameterError, + InvalidParameterError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -172,7 +172,7 @@ def param_serialize( # specified safe chars, encode everything encoded = quote(str(v), safe=config.safe_chars_for_path_param) if encoded in ('.', '..'): - raise InvalidPathParameterError(k, str(v)) + raise InvalidParameterError(k, str(v)) resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters diff --git a/fingerprint_server_sdk/exceptions.py b/fingerprint_server_sdk/exceptions.py index 7fce8132..76577ef5 100644 --- a/fingerprint_server_sdk/exceptions.py +++ b/fingerprint_server_sdk/exceptions.py @@ -236,16 +236,13 @@ class GatewayTimeoutException(ServiceException): pass -class InvalidPathParameterError(ApiValueError): +class InvalidParameterError(ApiValueError): """Exception when ``.`` or ``..`` used as resource identifier.""" def __init__(self, parameter: str, value: str) -> None: self.parameter = parameter self.value = value - super().__init__( - f'invalid value {value!r} for path parameter {parameter}: ' - 'path segment does not identify a resource' - ) + super().__init__(f'invalid value {value!r} for {parameter}: not a valid identifier') def render_path(path_to_item: list[Any]) -> str: diff --git a/template/__init__package.mustache b/template/__init__package.mustache index b52f5f66..36f9480f 100644 --- a/template/__init__package.mustache +++ b/template/__init__package.mustache @@ -26,7 +26,7 @@ __all__ = [ "TooManyRequestsException", "ServiceException", "GatewayTimeoutException", - "InvalidPathParameterError", + "InvalidParameterError", "WebhookValidation", "DecryptionKey", "DecryptionAlgorithm", diff --git a/template/api_client.mustache b/template/api_client.mustache index 1c485f73..b6d91c17 100644 --- a/template/api_client.mustache +++ b/template/api_client.mustache @@ -28,7 +28,7 @@ from {{packageName}}.configuration import Configuration from {{packageName}}.exceptions import ( ApiException, ApiValueError, - InvalidPathParameterError, + InvalidParameterError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -179,7 +179,7 @@ class ApiClient: # specified safe chars, encode everything encoded = quote(str(v), safe=config.safe_chars_for_path_param) if encoded in ('.', '..'): - raise InvalidPathParameterError(k, str(v)) + raise InvalidParameterError(k, str(v)) resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters diff --git a/template/api_doc.mustache b/template/api_doc.mustache index b31c7b6a..bd1fb941 100644 --- a/template/api_doc.mustache +++ b/template/api_doc.mustache @@ -34,9 +34,9 @@ Name | Type | Description | Notes {{#pathParams}} > [!WARNING] -> #### Invalid path parameter values +> #### Invalid values > -> `{{paramName}}` must not be `.` or `..`. Those values are relative path segments, so the call raises `InvalidPathParameterError` without sending a request. +> `{{paramName}}` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. {{/pathParams}} ### Return type diff --git a/template/exceptions.mustache b/template/exceptions.mustache index aee50af8..619de56f 100644 --- a/template/exceptions.mustache +++ b/template/exceptions.mustache @@ -222,16 +222,13 @@ class GatewayTimeoutException(ServiceException): pass -class InvalidPathParameterError(ApiValueError): +class InvalidParameterError(ApiValueError): """Exception when ``.`` or ``..`` used as resource identifier.""" def __init__(self, parameter: str, value: str) -> None: self.parameter = parameter self.value = value - super().__init__( - "invalid value {0!r} for path parameter {1}: " - "path segment does not identify a resource".format(value, parameter) - ) + super().__init__(f"invalid value {value!r} for {parameter}: not a valid identifier") def render_path(path_to_item: list[Any]) -> str: diff --git a/template/exports_package.mustache b/template/exports_package.mustache index e0b6818c..50e0f234 100644 --- a/template/exports_package.mustache +++ b/template/exports_package.mustache @@ -20,7 +20,7 @@ from {{packageName}}.exceptions import ConflictException from {{packageName}}.exceptions import UnprocessableEntityException from {{packageName}}.exceptions import TooManyRequestsException from {{packageName}}.exceptions import GatewayTimeoutException -from {{packageName}}.exceptions import InvalidPathParameterError +from {{packageName}}.exceptions import InvalidParameterError {{#hasHttpSignatureMethods}} from {{packageName}}.signing import HttpSigningConfiguration {{/hasHttpSignatureMethods}} diff --git a/test/test_path_params.py b/test/test_path_params.py index f9ccc516..a525f618 100644 --- a/test/test_path_params.py +++ b/test/test_path_params.py @@ -9,7 +9,7 @@ from fingerprint_server_sdk import ( Configuration, EventUpdate, - InvalidPathParameterError, + InvalidParameterError, ) from fingerprint_server_sdk.api.fingerprint_api import FingerprintApi @@ -163,7 +163,7 @@ def test_dot_segment_is_rejected_without_sending_a_request(self) -> None: with self.subTest(operation=operation.name, value=value): self.server.reset() - with self.assertRaises(InvalidPathParameterError) as context: + with self.assertRaises(InvalidParameterError) as context: operation.call(self.api, value) self.assertIsNone(self.server.request_target) From 57c23c8285893e105060400fabb8bc3f7c01d1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Tue, 22 Sep 2026 17:12:57 +0300 Subject: [PATCH 3/3] refactor: rename InvalidParameterError to InvalidArgumentError --- .changeset/reject-dot-segment-path-parameters.md | 2 +- docs/FingerprintApi.md | 6 +++--- fingerprint_server_sdk/__init__.py | 4 ++-- fingerprint_server_sdk/api_client.py | 4 ++-- fingerprint_server_sdk/exceptions.py | 8 ++++---- template/__init__package.mustache | 2 +- template/api_client.mustache | 4 ++-- template/api_doc.mustache | 2 +- template/exceptions.mustache | 8 ++++---- template/exports_package.mustache | 2 +- test/test_path_params.py | 16 ++++++++-------- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.changeset/reject-dot-segment-path-parameters.md b/.changeset/reject-dot-segment-path-parameters.md index 875c3936..f1432e3e 100644 --- a/.changeset/reject-dot-segment-path-parameters.md +++ b/.changeset/reject-dot-segment-path-parameters.md @@ -2,4 +2,4 @@ '@fingerprint/python-sdk': patch --- -Reject `.` and `..` as event and visitor IDs. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidParameterError` without sending a request. +Reject `.` and `..` as event and visitor IDs. `get_event`, `update_event`, and `delete_visitor_data` now raise the new `InvalidArgumentError` without sending a request. diff --git a/docs/FingerprintApi.md b/docs/FingerprintApi.md index 44de6e97..2e428eac 100644 --- a/docs/FingerprintApi.md +++ b/docs/FingerprintApi.md @@ -89,7 +89,7 @@ Name | Type | Description | Notes > [!WARNING] > #### Invalid values > -> `visitor_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. +> `visitor_id` must not be `.` or `..`. When you use them, the call raises `InvalidArgumentError` without sending a request. ### Return type @@ -172,7 +172,7 @@ Name | Type | Description | Notes > [!WARNING] > #### Invalid values > -> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. +> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidArgumentError` without sending a request. ### Return type @@ -474,7 +474,7 @@ Name | Type | Description | Notes > [!WARNING] > #### Invalid values > -> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. +> `event_id` must not be `.` or `..`. When you use them, the call raises `InvalidArgumentError` without sending a request. ### Return type diff --git a/fingerprint_server_sdk/__init__.py b/fingerprint_server_sdk/__init__.py index aa31ee70..ec609c87 100644 --- a/fingerprint_server_sdk/__init__.py +++ b/fingerprint_server_sdk/__init__.py @@ -37,7 +37,7 @@ 'TooManyRequestsException', 'ServiceException', 'GatewayTimeoutException', - 'InvalidParameterError', + 'InvalidArgumentError', 'WebhookValidation', 'DecryptionKey', 'DecryptionAlgorithm', @@ -129,7 +129,7 @@ from fingerprint_server_sdk.exceptions import UnprocessableEntityException from fingerprint_server_sdk.exceptions import TooManyRequestsException from fingerprint_server_sdk.exceptions import GatewayTimeoutException -from fingerprint_server_sdk.exceptions import InvalidParameterError +from fingerprint_server_sdk.exceptions import InvalidArgumentError # import models into sdk package from fingerprint_server_sdk.models.bot_info import BotInfo diff --git a/fingerprint_server_sdk/api_client.py b/fingerprint_server_sdk/api_client.py index 0918c053..1099e79c 100644 --- a/fingerprint_server_sdk/api_client.py +++ b/fingerprint_server_sdk/api_client.py @@ -37,7 +37,7 @@ from fingerprint_server_sdk.exceptions import ( ApiException, ApiValueError, - InvalidParameterError, + InvalidArgumentError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -172,7 +172,7 @@ def param_serialize( # specified safe chars, encode everything encoded = quote(str(v), safe=config.safe_chars_for_path_param) if encoded in ('.', '..'): - raise InvalidParameterError(k, str(v)) + raise InvalidArgumentError(k, str(v)) resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters diff --git a/fingerprint_server_sdk/exceptions.py b/fingerprint_server_sdk/exceptions.py index 76577ef5..db2acc65 100644 --- a/fingerprint_server_sdk/exceptions.py +++ b/fingerprint_server_sdk/exceptions.py @@ -236,13 +236,13 @@ class GatewayTimeoutException(ServiceException): pass -class InvalidParameterError(ApiValueError): +class InvalidArgumentError(ApiValueError): """Exception when ``.`` or ``..`` used as resource identifier.""" - def __init__(self, parameter: str, value: str) -> None: - self.parameter = parameter + def __init__(self, argument: str, value: str) -> None: + self.argument = argument self.value = value - super().__init__(f'invalid value {value!r} for {parameter}: not a valid identifier') + super().__init__(f'invalid value {value!r} for {argument}: not a valid identifier') def render_path(path_to_item: list[Any]) -> str: diff --git a/template/__init__package.mustache b/template/__init__package.mustache index 36f9480f..435894cc 100644 --- a/template/__init__package.mustache +++ b/template/__init__package.mustache @@ -26,7 +26,7 @@ __all__ = [ "TooManyRequestsException", "ServiceException", "GatewayTimeoutException", - "InvalidParameterError", + "InvalidArgumentError", "WebhookValidation", "DecryptionKey", "DecryptionAlgorithm", diff --git a/template/api_client.mustache b/template/api_client.mustache index b6d91c17..b6110c46 100644 --- a/template/api_client.mustache +++ b/template/api_client.mustache @@ -28,7 +28,7 @@ from {{packageName}}.configuration import Configuration from {{packageName}}.exceptions import ( ApiException, ApiValueError, - InvalidParameterError, + InvalidArgumentError, ) RequestSerialized = tuple[str, str, dict[str, Any], Optional[Any], Any] @@ -179,7 +179,7 @@ class ApiClient: # specified safe chars, encode everything encoded = quote(str(v), safe=config.safe_chars_for_path_param) if encoded in ('.', '..'): - raise InvalidParameterError(k, str(v)) + raise InvalidArgumentError(k, str(v)) resource_path = resource_path.replace('{' + k + '}', encoded) # post parameters diff --git a/template/api_doc.mustache b/template/api_doc.mustache index bd1fb941..d3e7a903 100644 --- a/template/api_doc.mustache +++ b/template/api_doc.mustache @@ -36,7 +36,7 @@ Name | Type | Description | Notes > [!WARNING] > #### Invalid values > -> `{{paramName}}` must not be `.` or `..`. When you use them, the call raises `InvalidParameterError` without sending a request. +> `{{paramName}}` must not be `.` or `..`. When you use them, the call raises `InvalidArgumentError` without sending a request. {{/pathParams}} ### Return type diff --git a/template/exceptions.mustache b/template/exceptions.mustache index 619de56f..3407fc13 100644 --- a/template/exceptions.mustache +++ b/template/exceptions.mustache @@ -222,13 +222,13 @@ class GatewayTimeoutException(ServiceException): pass -class InvalidParameterError(ApiValueError): +class InvalidArgumentError(ApiValueError): """Exception when ``.`` or ``..`` used as resource identifier.""" - def __init__(self, parameter: str, value: str) -> None: - self.parameter = parameter + def __init__(self, argument: str, value: str) -> None: + self.argument = argument self.value = value - super().__init__(f"invalid value {value!r} for {parameter}: not a valid identifier") + super().__init__(f"invalid value {value!r} for {argument}: not a valid identifier") def render_path(path_to_item: list[Any]) -> str: diff --git a/template/exports_package.mustache b/template/exports_package.mustache index 50e0f234..ac07250f 100644 --- a/template/exports_package.mustache +++ b/template/exports_package.mustache @@ -20,7 +20,7 @@ from {{packageName}}.exceptions import ConflictException from {{packageName}}.exceptions import UnprocessableEntityException from {{packageName}}.exceptions import TooManyRequestsException from {{packageName}}.exceptions import GatewayTimeoutException -from {{packageName}}.exceptions import InvalidParameterError +from {{packageName}}.exceptions import InvalidArgumentError {{#hasHttpSignatureMethods}} from {{packageName}}.signing import HttpSigningConfiguration {{/hasHttpSignatureMethods}} diff --git a/test/test_path_params.py b/test/test_path_params.py index a525f618..e52d4c12 100644 --- a/test/test_path_params.py +++ b/test/test_path_params.py @@ -9,7 +9,7 @@ from fingerprint_server_sdk import ( Configuration, EventUpdate, - InvalidParameterError, + InvalidArgumentError, ) from fingerprint_server_sdk.api.fingerprint_api import FingerprintApi @@ -23,7 +23,7 @@ class PathParamOperation: """An operation that takes an ID as a URL path parameter.""" name: str - param: str + argument: str prefix: str call: Callable[[FingerprintApi, str], object] @@ -31,19 +31,19 @@ class PathParamOperation: OPERATIONS = ( PathParamOperation( name='get_event', - param='event_id', + argument='event_id', prefix='/events/', call=lambda api, event_id: api.get_event(event_id), ), PathParamOperation( name='update_event', - param='event_id', + argument='event_id', prefix='/events/', call=lambda api, event_id: api.update_event(event_id, EventUpdate(suspect=True)), ), PathParamOperation( name='delete_visitor_data', - param='visitor_id', + argument='visitor_id', prefix='/visitors/', call=lambda api, visitor_id: api.delete_visitor_data(visitor_id), ), @@ -163,13 +163,13 @@ def test_dot_segment_is_rejected_without_sending_a_request(self) -> None: with self.subTest(operation=operation.name, value=value): self.server.reset() - with self.assertRaises(InvalidParameterError) as context: + with self.assertRaises(InvalidArgumentError) as context: operation.call(self.api, value) self.assertIsNone(self.server.request_target) - self.assertEqual(operation.param, context.exception.parameter) + self.assertEqual(operation.argument, context.exception.argument) self.assertEqual(value, context.exception.value) - self.assertIn(operation.param, str(context.exception)) + self.assertIn(operation.argument, str(context.exception)) def test_empty_value_does_not_address_the_collection(self) -> None: """An empty ID leaves a trailing slash"""