Skip to content

feat: add structured error codes and context to ActionExecutionResult#459

Open
rjmunro wants to merge 4 commits into
Sofie-Automation:mainfrom
bbc:rjmunro/improve-device-error-notifications
Open

feat: add structured error codes and context to ActionExecutionResult#459
rjmunro wants to merge 4 commits into
Sofie-Automation:mainfrom
bbc:rjmunro/improve-device-error-notifications

Conversation

@rjmunro
Copy link
Copy Markdown
Contributor

@rjmunro rjmunro commented May 19, 2026

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

This is a: Feature / Code improvement

Current Behavior

When a device action fails (e.g. CasparCG or HTTP Send), the ActionExecutionResult returned to the caller contains only a pre-rendered human-readable message. There is no structured way for consumers (e.g. blueprints) to identify the specific error type or to customise the error message for their context.

New Behavior

ActionExecutionResult gains two optional fields:

  • code — a structured error code following the pattern ACTION_{DEVICETYPE}_{REASON} (e.g. ACTION_CASPARCG_LAUNCHER_HOST_NOT_SET)
  • context — a Record<string, unknown> providing values for custom message interpolation via interpolateTemplateString()

The response field continues to carry a pre-rendered fallback message, so existing consumers are unaffected. Consumers that want to customise messages can match on code and interpolate using context.

Both CasparCG and HTTP Send device integrations have been updated to return structured error codes and context on their action errors.

Additionally, the ws package has been bumped to address a vulnerability report (GHSA).

Testing Instructions

  • Trigger a failing CasparCG action (e.g. with no launcher host configured) and verify ActionExecutionResult includes a code and context alongside the existing response message.
  • Trigger a failing HTTP Send action and verify the same.
  • Verify that existing consumers that only use response continue to work unchanged.
  • Run the unit tests: yarn unit

Other Information

The code/context fields are purely additive and optional, making this change fully backwards-compatible. A blueprint can opt in to richer error messages by looking up code in a deviceActionErrorMessages map and interpolating with context.

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Review Change Stack

Walkthrough

This PR adds structured error reporting to device action execution results. The ActionExecutionResult interface gains optional code and context fields for customizable error messages. CasparCG and HttpSend devices now return specific error codes, interpolation context, and localized response text instead of generic error results.

Changes

Structured error support for device actions

Layer / File(s) Summary
Core error handling contract
packages/timeline-state-resolver-types/src/actions.ts
ActionExecutionResult interface updated with optional code?: string and context?: Record<string, unknown> fields enabling device actions to return structured error information for client-side error message interpolation.
CasparCG error codes and message templates
packages/timeline-state-resolver-types/src/integrations/casparcg.ts
Introduces CasparCGActionErrorCode enum with codes for ClearAllChannels, RestartServer, and ListMedia; adds default message templates and context shape definitions mapping error codes to interpolation data (e.g., HTTP status, error message).
CasparCG device action error handling
packages/timeline-state-resolver/src/integrations/casparCG/index.ts
clearAllChannels, restartCasparCG, and listMedia methods now return structured errors with code, context, and localized response text for connection failures, configuration validation, launcher communication, and CLS command execution.
HttpSend error codes and message templates
packages/timeline-state-resolver-types/src/integrations/httpSend.ts
Introduces HttpSendActionErrorCode enum covering request validation and failure cases; includes default templated messages for payload, URL, params validation and HTTP request failures.
HttpSend device action error handling
packages/timeline-state-resolver/src/integrations/httpSend/index.ts
executeSendCommandAction and sendCommandWithResult methods now return structured errors with codes for input validation failures and HTTP request errors; includes error context (URL, error message, optional error code) and localized response text. Timing logic refactored to use explicit startTime variable for retry calculations.

Dependencies

  • ws dependency in packages/timeline-state-resolver/package.json bumped from ^8.19.0 to ^8.20.1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

contribution from BBC

Suggested reviewers

  • jstarpl
  • chandrashekar-nallamilli
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding structured error codes and context support to ActionExecutionResult for better error handling.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem, new behavior, backwards compatibility, and testing instructions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

@rjmunro rjmunro marked this pull request as ready for review May 22, 2026 09:11
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/timeline-state-resolver/src/integrations/casparCG/index.ts (1)

744-764: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix RESTART_BAD_REPLY reachability for non-2xx responses
With got v11 defaulting throwHttpErrors: true, 3xx/4xx/5xx responses reject before this .then, so the else branch only runs for 2xx responses where statusCode !== 200 (e.g., 201/204). If RESTART_BAD_REPLY should cover non-2xx as well, set throwHttpErrors: false (or map got.HTTPError in .catch).

Proposed fix
 		return got
 			.post(url, {
 				timeout: {
 					request: 5000, // Arbitary, long enough for realistic scenarios
 				},
+				throwHttpErrors: false,
 			})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timeline-state-resolver/src/integrations/casparCG/index.ts` around
lines 744 - 764, The current got.post(...) call relies on a .then(...) to map
non-200 replies to CasparCGActionErrorCode.RESTART_BAD_REPLY, but got v11 throws
on non-2xx by default so those responses never reach the .then; either disable
throwing by adding throwHttpErrors: false to the options object passed to
got.post, or keep the default and add a .catch that detects got.HTTPError and
maps its response (statusCode and body) to the same error shape (using
ActionExecutionResultCode.Error and CasparCGActionErrorCode.RESTART_BAD_REPLY)
so that non-2xx replies are handled consistently instead of being swallowed by
the rejected promise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/timeline-state-resolver-types/src/integrations/casparcg.ts`:
- Around line 46-49: The docstring currently refers to the obsolete config key
deviceActionMessages; update the comment to use the correct blueprint config key
deviceActionErrorMessages everywhere in this file (e.g., in the top-level
comment describing "Action error codes for CasparCG device actions") so the
documentation matches the PR and related docs and avoid any mismatches for
CasparCG integration consumers.

In `@packages/timeline-state-resolver/src/integrations/casparCG/index.ts`:
- Around line 645-657: The two early error returns in clearAllChannels
(returning ActionExecutionResultCode.Error with
CasparCGActionErrorCode.CLEAR_INFO_FAILED and
CasparCGActionErrorCode.CLEAR_NO_CHANNELS) omit the legacy fallback "response"
field; update those returned objects to include the same fallback response value
used elsewhere (e.g., the variable response or its fallback message) so callers
relying on the legacy response still receive it—modify the return statements in
clearAllChannels that reference CLEAR_INFO_FAILED and CLEAR_NO_CHANNELS to
include a response property mirroring the successful-path response fallback.

In `@packages/timeline-state-resolver/src/integrations/httpSend/index.ts`:
- Around line 117-123: The validation mistakenly checks cmd.type instead of
cmd.paramsType; update the guard in the HTTP send handler so it verifies that
cmd.paramsType exists and is a valid key in TimelineContentTypeHTTPParamType
(i.e., replace the check "!(cmd.type in TimelineContentTypeHTTPParamType)" with
a check against cmd.paramsType) so the error return (using
ActionExecutionResultCode.Error and HttpSendActionErrorCode.INVALID_PARAMS_TYPE)
only fires for truly invalid paramsType values.

---

Outside diff comments:
In `@packages/timeline-state-resolver/src/integrations/casparCG/index.ts`:
- Around line 744-764: The current got.post(...) call relies on a .then(...) to
map non-200 replies to CasparCGActionErrorCode.RESTART_BAD_REPLY, but got v11
throws on non-2xx by default so those responses never reach the .then; either
disable throwing by adding throwHttpErrors: false to the options object passed
to got.post, or keep the default and add a .catch that detects got.HTTPError and
maps its response (statusCode and body) to the same error shape (using
ActionExecutionResultCode.Error and CasparCGActionErrorCode.RESTART_BAD_REPLY)
so that non-2xx replies are handled consistently instead of being swallowed by
the rejected promise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb315daa-3c7f-4122-91a4-64c08559d0b1

📥 Commits

Reviewing files that changed from the base of the PR and between 5f60335 and c4c70af.

⛔ Files ignored due to path filters (3)
  • packages/timeline-state-resolver-types/src/__tests__/__snapshots__/index.spec.ts.snap is excluded by !**/*.snap
  • packages/timeline-state-resolver/src/__tests__/__snapshots__/index.spec.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • packages/timeline-state-resolver-types/src/actions.ts
  • packages/timeline-state-resolver-types/src/integrations/casparcg.ts
  • packages/timeline-state-resolver-types/src/integrations/httpSend.ts
  • packages/timeline-state-resolver/package.json
  • packages/timeline-state-resolver/src/integrations/casparCG/index.ts
  • packages/timeline-state-resolver/src/integrations/httpSend/index.ts

Comment on lines +46 to +49
/**
* Action error codes for CasparCG device actions.
* These codes can be customized in blueprints via deviceActionMessages.
*
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix blueprint config key name in the docstring.

The comment references deviceActionMessages, but this PR and related docs use deviceActionErrorMessages. Please align the term to avoid integration confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timeline-state-resolver-types/src/integrations/casparcg.ts` around
lines 46 - 49, The docstring currently refers to the obsolete config key
deviceActionMessages; update the comment to use the correct blueprint config key
deviceActionErrorMessages everywhere in this file (e.g., in the top-level
comment describing "Action error codes for CasparCG device actions") so the
documentation matches the PR and related docs and avoid any mismatches for
CasparCG integration consumers.

Comment on lines +645 to +657
return {
result: ActionExecutionResultCode.Error,
code: CasparCGActionErrorCode.CLEAR_INFO_FAILED,
context: {},
}
}
const response = await request
if (!response?.data[0]) {
return { result: ActionExecutionResultCode.Error }
return {
result: ActionExecutionResultCode.Error,
code: CasparCGActionErrorCode.CLEAR_NO_CHANNELS,
context: {},
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return fallback response on all error paths in clearAllChannels.

Two structured error returns omit response, which weakens backward compatibility for consumers still relying on the legacy fallback message.

Proposed fix
 		if (error) {
 			return {
 				result: ActionExecutionResultCode.Error,
 				code: CasparCGActionErrorCode.CLEAR_INFO_FAILED,
 				context: {},
+				response: t('Cannot clear CasparCG channels: failed to retrieve channel info'),
 			}
 		}
@@
 		if (!response?.data[0]) {
 			return {
 				result: ActionExecutionResultCode.Error,
 				code: CasparCGActionErrorCode.CLEAR_NO_CHANNELS,
 				context: {},
+				response: t('Cannot clear CasparCG channels: no channels found'),
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timeline-state-resolver/src/integrations/casparCG/index.ts` around
lines 645 - 657, The two early error returns in clearAllChannels (returning
ActionExecutionResultCode.Error with CasparCGActionErrorCode.CLEAR_INFO_FAILED
and CasparCGActionErrorCode.CLEAR_NO_CHANNELS) omit the legacy fallback
"response" field; update those returned objects to include the same fallback
response value used elsewhere (e.g., the variable response or its fallback
message) so callers relying on the legacy response still receive it—modify the
return statements in clearAllChannels that reference CLEAR_INFO_FAILED and
CLEAR_NO_CHANNELS to include a response property mirroring the successful-path
response fallback.

Comment on lines 117 to 123
if (cmd.paramsType && !(cmd.type in TimelineContentTypeHTTPParamType)) {
return {
result: ActionExecutionResultCode.Error,
response: t('Failed to send command: params type is invalid'),
code: HttpSendActionErrorCode.INVALID_PARAMS_TYPE,
context: { paramsType: cmd.paramsType },
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix params-type validation condition (wrong field checked).

The guard checks cmd.type instead of cmd.paramsType, so valid paramsType values can be incorrectly rejected.

Proposed fix
-		if (cmd.paramsType && !(cmd.type in TimelineContentTypeHTTPParamType)) {
+		if (
+			cmd.paramsType &&
+			!Object.values<TimelineContentTypeHTTPParamType>(TimelineContentTypeHTTPParamType).includes(cmd.paramsType)
+		) {
 			return {
 				result: ActionExecutionResultCode.Error,
 				response: t('Failed to send command: params type is invalid'),
 				code: HttpSendActionErrorCode.INVALID_PARAMS_TYPE,
 				context: { paramsType: cmd.paramsType },
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (cmd.paramsType && !(cmd.type in TimelineContentTypeHTTPParamType)) {
return {
result: ActionExecutionResultCode.Error,
response: t('Failed to send command: params type is invalid'),
code: HttpSendActionErrorCode.INVALID_PARAMS_TYPE,
context: { paramsType: cmd.paramsType },
}
if (
cmd.paramsType &&
!Object.values<TimelineContentTypeHTTPParamType>(TimelineContentTypeHTTPParamType).includes(cmd.paramsType)
) {
return {
result: ActionExecutionResultCode.Error,
response: t('Failed to send command: params type is invalid'),
code: HttpSendActionErrorCode.INVALID_PARAMS_TYPE,
context: { paramsType: cmd.paramsType },
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timeline-state-resolver/src/integrations/httpSend/index.ts` around
lines 117 - 123, The validation mistakenly checks cmd.type instead of
cmd.paramsType; update the guard in the HTTP send handler so it verifies that
cmd.paramsType exists and is a valid key in TimelineContentTypeHTTPParamType
(i.e., replace the check "!(cmd.type in TimelineContentTypeHTTPParamType)" with
a check against cmd.paramsType) so the error return (using
ActionExecutionResultCode.Error and HttpSendActionErrorCode.INVALID_PARAMS_TYPE)
only fires for truly invalid paramsType values.

@Saftret Saftret added the contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution from BBC Contributions sponsored by BBC (bbc.co.uk)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants