Skip to content

Commit 8c1bab3

Browse files
Merge pull request #1640 from roboflow/feature/custom-error-handler-in-workflows
Add custom error handlers in Workflows
2 parents 3263387 + fc818a2 commit 8c1bab3

File tree

26 files changed

+427
-49
lines changed

26 files changed

+427
-49
lines changed

docs/workflows/execution_engine_changelog.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
Below you can find the changelog for Execution Engine.
44

5+
## Execution Engine `v1.7.0` | inference `v0.59.0`
6+
7+
!!! warning "Breaking change regarding step errors in workflows"
8+
9+
To fix a bug related to invalid HTTP responses codes in `inference-server` handling Workflows execution requests
10+
we needed to alter the default mechanism responsible for handling errors in Execution Engine. As a result of change,
11+
effective immediately on Roboflow Hosted Platform and in `inference>=0.59.0`, Workflow blocks interacting with
12+
Roboflow platform which fails due to client misconfiguration (invalid Roboflow API key, invalid model ID, etc.)
13+
instead of raising `StepExecutionError` (and HTTP 500 response from the server) will raise
14+
`ClientCausedStepExecutionError` (and relevant HTTP response codes, such as 400, 401, 403, 404).
15+
16+
List of scenarios affected with the change:
17+
18+
* Block using Roboflow model defines invalid model ID - now will raise `ClientCausedStepExecutionError` with status code 400
19+
20+
* Block using Roboflow model defines invalid API key - now will raise `ClientCausedStepExecutionError` with status code 401
21+
22+
* Block using Roboflow model defines invalid API key or missing valid key with scpe to access resource - now will raise
23+
`ClientCausedStepExecutionError` with status code 403
24+
25+
* Block using Roboflow model defines model which does not exist - now will raise
26+
`ClientCausedStepExecutionError` with status code 404
27+
28+
29+
!!! Note "Bringing back `legacy` error handling"
30+
31+
It is possible to bring back the legacy behaviour of error handler if needed, which may be halpful in transition
32+
period - all it takes is setting environmental variable `DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER=legacy`.
33+
34+
535
## Execution Engine `v1.6.0` | inference `v0.53.0`
636

737
!!! Note "Change may require attention"
@@ -552,5 +582,3 @@ include a new `video_metadata` property. This property can be optionally set in
552582
a default value with reasonable defaults will be used. To simplify metadata manipulation within blocks, we have
553583
introduced two new class methods: `WorkflowImageData.copy_and_replace(...)` and `WorkflowImageData.create_crop(...)`.
554584
For more details, refer to the updated [`WoorkflowImageData` usage guide](/workflows/internal_data_types.md#workflowimagedata).
555-
556-

inference/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ class RoboflowAPINotAuthorizedError(RoboflowAPIUnsuccessfulRequestError):
151151
pass
152152

153153

154+
class RoboflowAPIForbiddenError(RoboflowAPIUnsuccessfulRequestError):
155+
pass
156+
157+
154158
class RoboflowAPINotNotFoundError(RoboflowAPIUnsuccessfulRequestError):
155159
pass
156160

inference/core/interfaces/http/error_handlers.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
PostProcessingError,
2323
PreProcessingError,
2424
RoboflowAPIConnectionError,
25+
RoboflowAPIForbiddenError,
2526
RoboflowAPINotAuthorizedError,
2627
RoboflowAPINotNotFoundError,
2728
RoboflowAPITimeoutError,
@@ -45,6 +46,7 @@
4546
OperationTypeNotRecognisedError,
4647
)
4748
from inference.core.workflows.errors import (
49+
ClientCausedStepExecutionError,
4850
DynamicBlockError,
4951
ExecutionGraphStructureError,
5052
InvalidReferenceTargetError,
@@ -187,6 +189,16 @@ def wrapped_route(*args, **kwargs):
187189
"to learn how to retrieve one."
188190
},
189191
)
192+
except RoboflowAPIForbiddenError as error:
193+
logger.exception("%s: %s", type(error).__name__, error)
194+
resp = JSONResponse(
195+
status_code=403,
196+
content={
197+
"message": "Unauthorized access to roboflow API - check API key and make sure the key is valid and "
198+
"have required scopes. Visit https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key "
199+
"to learn how to retrieve one."
200+
},
201+
)
190202
except RoboflowAPINotNotFoundError as error:
191203
logger.exception("%s: %s", type(error).__name__, error)
192204
resp = JSONResponse(
@@ -284,6 +296,24 @@ def wrapped_route(*args, **kwargs):
284296
"message": "Timeout when attempting to connect to Roboflow API."
285297
},
286298
)
299+
except ClientCausedStepExecutionError as error:
300+
logger.exception("%s: %s", type(error).__name__, error)
301+
content = WorkflowErrorResponse(
302+
message=str(error.public_message),
303+
error_type=error.__class__.__name__,
304+
context=str(error.context),
305+
inner_error_type=str(error.inner_error_type),
306+
inner_error_message=str(error.inner_error),
307+
blocks_errors=[
308+
WorkflowBlockError(
309+
block_id=error.block_id,
310+
),
311+
],
312+
)
313+
resp = JSONResponse(
314+
status_code=error.status_code,
315+
content=content.model_dump(),
316+
)
287317
except StepExecutionError as error:
288318
logger.exception("%s: %s", type(error).__name__, error)
289319
content = WorkflowErrorResponse(
@@ -462,6 +492,16 @@ async def wrapped_route(*args, **kwargs):
462492
"to learn how to retrieve one."
463493
},
464494
)
495+
except RoboflowAPIForbiddenError as error:
496+
logger.exception("%s: %s", type(error).__name__, error)
497+
resp = JSONResponse(
498+
status_code=403,
499+
content={
500+
"message": "Unauthorized access to roboflow API - check API key and make sure the key is valid and "
501+
"have required scopes. Visit https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key "
502+
"to learn how to retrieve one."
503+
},
504+
)
465505
except RoboflowAPINotNotFoundError as error:
466506
logger.exception("%s: %s", type(error).__name__, error)
467507
resp = JSONResponse(
@@ -559,6 +599,24 @@ async def wrapped_route(*args, **kwargs):
559599
"message": "Timeout when attempting to connect to Roboflow API."
560600
},
561601
)
602+
except ClientCausedStepExecutionError as error:
603+
logger.exception("%s: %s", type(error).__name__, error)
604+
content = WorkflowErrorResponse(
605+
message=str(error.public_message),
606+
error_type=error.__class__.__name__,
607+
context=str(error.context),
608+
inner_error_type=str(error.inner_error_type),
609+
inner_error_message=str(error.inner_error),
610+
blocks_errors=[
611+
WorkflowBlockError(
612+
block_id=error.block_id,
613+
),
614+
],
615+
)
616+
resp = JSONResponse(
617+
status_code=error.status_code,
618+
content=content.model_dump(),
619+
)
562620
except StepExecutionError as error:
563621
logger.exception("%s: %s", type(error).__name__, error)
564622
content = WorkflowErrorResponse(

inference/core/roboflow_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
MissingDefaultModelError,
5454
RetryRequestError,
5555
RoboflowAPIConnectionError,
56+
RoboflowAPIForbiddenError,
5657
RoboflowAPIIAlreadyAnnotatedError,
5758
RoboflowAPIIAnnotationRejectionError,
5859
RoboflowAPIImageUploadRejectionError,
@@ -97,6 +98,12 @@ def raise_from_lambda(
9798
"Unauthorized access to roboflow API - check API key. Visit "
9899
"https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key to learn how to retrieve one.",
99100
),
101+
403: lambda e: raise_from_lambda(
102+
e,
103+
RoboflowAPIForbiddenError,
104+
"Unauthorized access to roboflow API - check API key regarding correctness and required scopes. Visit "
105+
"https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key to learn how to retrieve one.",
106+
),
100107
404: lambda e: raise_from_lambda(
101108
e, RoboflowAPINotNotFoundError, NOT_FOUND_ERROR_MESSAGE
102109
),

inference/core/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.58.3"
1+
__version__ = "0.59.0"
22

33

44
if __name__ == "__main__":

inference/core/workflows/core_steps/sinks/onvif_movement/v1.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@
1717
from inference.core import logger
1818
from inference.core.utils.function import experimental
1919
from inference.core.workflows.core_steps.common.entities import StepExecutionMode
20-
from inference.core.workflows.core_steps.common.query_language.entities.operations import (
21-
AllOperationsType,
22-
)
23-
from inference.core.workflows.core_steps.common.query_language.operations.core import (
24-
build_operations_chain,
25-
)
26-
from inference.core.workflows.errors import StepExecutionError, WorkflowError
2720
from inference.core.workflows.execution_engine.entities.base import OutputDefinition
2821
from inference.core.workflows.execution_engine.entities.types import (
2922
BOOLEAN_KIND,
@@ -403,9 +396,7 @@ async def configure_async(self):
403396
Doesn't currently run in init since it needs to be awaited
404397
"""
405398
if not self.camera:
406-
raise StepExecutionError(
407-
f"Tried to configure camera, but camera was not created"
408-
)
399+
raise ValueError(f"Tried to configure camera, but camera was not created")
409400
if self._has_config_error:
410401
return
411402

inference/core/workflows/errors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,22 @@ def __init__(
173173
self.block_type = block_type
174174

175175

176+
class ClientCausedStepExecutionError(WorkflowExecutionEngineError):
177+
def __init__(
178+
self,
179+
block_id: str,
180+
status_code: int,
181+
public_message: str,
182+
context: str,
183+
inner_error: Optional[Exception] = None,
184+
):
185+
super().__init__(
186+
public_message=public_message, context=context, inner_error=inner_error
187+
)
188+
self.block_id = block_id
189+
self.status_code = status_code
190+
191+
176192
class ExecutionEngineRuntimeError(WorkflowExecutionEngineError):
177193
pass
178194

inference/core/workflows/execution_engine/core.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from concurrent.futures import ThreadPoolExecutor
2-
from typing import Any, Dict, List, Optional, Type
2+
from typing import Any, Callable, Dict, List, Optional, Type, Union
33

44
from packaging.specifiers import SpecifierSet
55
from packaging.version import Version
@@ -14,6 +14,7 @@
1414
)
1515
from inference.core.workflows.execution_engine.profiling.core import WorkflowsProfiler
1616
from inference.core.workflows.execution_engine.v1.core import (
17+
DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER,
1718
EXECUTION_ENGINE_V1_VERSION,
1819
ExecutionEngineV1,
1920
)
@@ -39,6 +40,9 @@ def init(
3940
workflow_id: Optional[str] = None,
4041
profiler: Optional[WorkflowsProfiler] = None,
4142
executor: Optional[ThreadPoolExecutor] = None,
43+
step_error_handler: Optional[
44+
Union[str, Callable[[str, Exception], None]]
45+
] = DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER,
4246
) -> "ExecutionEngine":
4347
requested_engine_version = retrieve_requested_execution_engine_version(
4448
workflow_definition=workflow_definition,
@@ -54,6 +58,7 @@ def init(
5458
workflow_id=workflow_id,
5559
profiler=profiler,
5660
executor=executor,
61+
step_error_handler=step_error_handler,
5762
)
5863
return cls(engine=engine)
5964

inference/core/workflows/execution_engine/entities/engine.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from abc import ABC, abstractmethod
22
from concurrent.futures import ThreadPoolExecutor
3-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Callable, Dict, List, Optional, Union
44

55
from inference.core.workflows.execution_engine.profiling.core import WorkflowsProfiler
66

@@ -18,6 +18,9 @@ def init(
1818
workflow_id: Optional[str] = None,
1919
profiler: Optional[WorkflowsProfiler] = None,
2020
executor: Optional[ThreadPoolExecutor] = None,
21+
step_error_handler: Optional[
22+
Union[str, Callable[[str, Exception], None]]
23+
] = None,
2124
) -> "BaseExecutionEngine":
2225
pass
2326

inference/core/workflows/execution_engine/v1/core.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import os
12
from concurrent.futures import ThreadPoolExecutor
2-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Callable, Dict, List, Optional, Union
34

45
from packaging.version import Version
56

67
from inference.core.logger import logger
8+
from inference.core.workflows.errors import WorkflowEnvironmentConfigurationError
79
from inference.core.workflows.execution_engine.entities.engine import (
810
BaseExecutionEngine,
911
)
@@ -22,8 +24,21 @@
2224
from inference.core.workflows.execution_engine.v1.executor.runtime_input_validator import (
2325
validate_runtime_input,
2426
)
27+
from inference.core.workflows.execution_engine.v1.step_error_handlers import (
28+
extended_roboflow_errors_handler,
29+
legacy_step_error_handler,
30+
)
31+
32+
EXECUTION_ENGINE_V1_VERSION = Version("1.7.0")
2533

26-
EXECUTION_ENGINE_V1_VERSION = Version("1.6.0")
34+
DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER = os.getenv(
35+
"DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER", "extended_roboflow_errors"
36+
)
37+
38+
REGISTERED_STEP_ERROR_HANDLERS = {
39+
"legacy": legacy_step_error_handler,
40+
"extended_roboflow_errors": extended_roboflow_errors_handler,
41+
}
2742

2843

2944
class ExecutionEngineV1(BaseExecutionEngine):
@@ -38,10 +53,21 @@ def init(
3853
workflow_id: Optional[str] = None,
3954
profiler: Optional[WorkflowsProfiler] = None,
4055
executor: Optional[ThreadPoolExecutor] = None,
56+
step_error_handler: Optional[
57+
Union[str, Callable[[str, Exception], None]]
58+
] = DEFAULT_WORKFLOWS_STEP_ERROR_HANDLER,
4159
) -> "ExecutionEngineV1":
4260
if init_parameters is None:
4361
init_parameters = {}
44-
62+
if isinstance(step_error_handler, str):
63+
if step_error_handler not in REGISTERED_STEP_ERROR_HANDLERS:
64+
raise WorkflowEnvironmentConfigurationError(
65+
public_message=f"Execution engine was initialised with step_error_handler='{step_error_handler}' "
66+
f"which is not registered. Supported values: "
67+
f"{list(REGISTERED_STEP_ERROR_HANDLERS.keys())}",
68+
context="workflow_compilation | engine_initialisation",
69+
)
70+
step_error_handler = REGISTERED_STEP_ERROR_HANDLERS[step_error_handler]
4571
init_parameters["dynamic_workflows_blocks.api_key"] = init_parameters.get(
4672
"dynamic_workflows_blocks.api_key",
4773
init_parameters.get("workflows_core.api_key"),
@@ -63,6 +89,7 @@ def init(
6389
workflow_id=workflow_id,
6490
internal_id=workflow_definition.get("id"),
6591
executor=executor,
92+
step_error_handler=step_error_handler,
6693
)
6794

6895
def __init__(
@@ -74,6 +101,7 @@ def __init__(
74101
workflow_id: Optional[str] = None,
75102
internal_id: Optional[str] = None,
76103
executor: Optional[ThreadPoolExecutor] = None,
104+
step_error_handler: Optional[Callable[[str, Exception], None]] = None,
77105
):
78106
self._compiled_workflow = compiled_workflow
79107
self._max_concurrent_steps = max_concurrent_steps
@@ -82,6 +110,7 @@ def __init__(
82110
self._profiler = profiler
83111
self._internal_id = internal_id
84112
self._executor = executor
113+
self._step_error_handler = step_error_handler
85114

86115
def run(
87116
self,
@@ -121,6 +150,7 @@ def run(
121150
serialize_results=serialize_results,
122151
profiler=self._profiler,
123152
executor=self._executor,
153+
step_error_handler=self._step_error_handler,
124154
)
125155
self._profiler.end_workflow_run()
126156
return result

0 commit comments

Comments
 (0)