Skip to content

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

Merged
rjmunro merged 4 commits into
Sofie-Automation:mainfrom
bbc:rjmunro/improve-device-error-notifications
Jun 2, 2026
Merged

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

Conversation

@rjmunro

@rjmunro rjmunro commented May 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rjmunro, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 45 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc7b3d99-de0d-41c5-82ae-47f07fe489dc

📥 Commits

Reviewing files that changed from the base of the PR and between 911fed7 and 47392d5.

📒 Files selected for processing (1)
  • packages/timeline-state-resolver/src/integrations/httpSend/index.ts

Walkthrough

This change unifies device action error handling by introducing structured error codes, human-readable message templates, and error interpolation contexts for both CasparCG and HTTP Send devices. Device action implementations are updated to return these structured errors in all major failure scenarios.

Changes

Structured error support for CasparCG and HTTP Send device actions

Layer / File(s) Summary
Structured error result contract
packages/timeline-state-resolver-types/src/actions.ts
Expands the ActionExecutionResult interface with optional code and context fields for detailed error output, documents error message contract via JSDoc.
CasparCG device action error codes and error handling
packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.ts,
packages/timeline-state-resolver/src/integrations/casparCG/index.ts
Defines CasparCGActionErrorCode, message templates, context map, and enhances all CasparCG device actions (clearAllChannels, restartCasparCG, listMedia) to yield domain-specific error codes, contextual data, and pre-rendered responses for all error paths.
HttpSend action error codes and implementation
packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts,
packages/timeline-state-resolver/src/integrations/httpSend/index.ts
Adds HttpSendActionErrorCode, error message plumbing, context interface, and updates HTTPSend device logic to return detailed codes/contexts for all validation and HTTP request errors, including templated error responses.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant clearAllChannels
  participant restartCasparCG
  participant listMedia
  Caller->>clearAllChannels: invoke
  alt No active connection
    clearAllChannels->>Caller: { result: Error, code: CLEAR_NO_CONNECTION, response }
  else Commands.Info fails
    clearAllChannels->>Caller: { result: Error, code: CLEAR_FAILED, response }
  else No channel data
    clearAllChannels->>Caller: { result: Error, code: CLEAR_FAILED, response }
  end
  Caller->>restartCasparCG: invoke
  alt Missing initOptions
    restartCasparCG->>Caller: { result: Error, code: RESTART_NOT_INITIALIZED, response }
  else Missing launcher setting
    restartCasparCG->>Caller: { result: Error, code: RESTART_MISSING_LAUNCHER_SETTINGS, response }
  else Bad launcher reply
    restartCasparCG->>Caller: { result: Error, code: RESTART_BAD_REPLY, context: { statusCode, body }, response }
  else Launcher request fails
    restartCasparCG->>Caller: { result: Error, code: RESTART_REQUEST_FAILED, context: { errorMessage }, response }
  end
  Caller->>listMedia: invoke
  alt Device not initialized
    listMedia->>Caller: { result: Error, code: LIST_NOT_INITIALIZED, response }
  else CLS command fails
    listMedia->>Caller: { result: Error, code: LIST_CLS_ERROR, context: { errorMessage }, response }
  else Bad server response
    listMedia->>Caller: { result: Error, code: LIST_BAD_RESPONSE, context: { responseCode }, response }
  end
Loading
sequenceDiagram
  participant Caller
  participant executeSendCommandAction
  participant sendCommandWithResult
  Caller->>executeSendCommandAction: invoke with command
  alt Invalid payload/url/type/params
    executeSendCommandAction->>Caller: { result: Error, code: (PAYLOAD_INVALID/URL_INVALID/TYPE_INVALID/PARAMS_INVALID/PARAMSTYPE_INVALID), context, response }
  else Valid, send request
    executeSendCommandAction->>sendCommandWithResult: send()
    alt HTTP request success
      sendCommandWithResult->>Caller: { result: Ok }
    else HTTP request fails
      sendCommandWithResult->>Caller: { result: Error, code: REQUEST_FAILED, context: { url, errorMessage[, errorCode] }, response }
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested reviewers

  • Julusian
  • jstarpl
  • nytamin
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely describes the main change: adding structured error codes and context to ActionExecutionResult interface.
Description check ✅ Passed The description is comprehensive and clearly related to the changeset, explaining current behavior, new behavior with code and context fields, 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

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.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 thread packages/timeline-state-resolver/src/integrations/casparCG/index.ts
Comment thread packages/timeline-state-resolver/src/integrations/httpSend/index.ts Outdated
@Saftret Saftret added the contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label May 28, 2026
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from c4c70af to 911fed7 Compare June 2, 2026 13:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts (1)

41-61: ⚡ Quick win

Default messages discard the context they’re given.

The context map exposes interpolation values for INVALID_TYPE (type), MISSING_PARAMS (url), and INVALID_PARAMS_TYPE (paramsType), but the corresponding default messages on lines 44–46 don’t reference any of them. Only REQUEST_FAILED actually consumes its context. As a result the default fallback strings lose useful diagnostic detail (which method/url/params type was invalid), even though the data is plumbed through. Either interpolate these values into the defaults or drop them from the context map to keep the contract consistent.

♻️ Proposed message templates
-	[HttpSendActionErrorCode.INVALID_TYPE]: 'Failed to send HTTP command: invalid HTTP method type',
-	[HttpSendActionErrorCode.MISSING_PARAMS]: 'Failed to send HTTP command: missing params',
-	[HttpSendActionErrorCode.INVALID_PARAMS_TYPE]: 'Failed to send HTTP command: invalid params type',
+	[HttpSendActionErrorCode.INVALID_TYPE]: 'Failed to send HTTP command: invalid HTTP method type "{{type}}"',
+	[HttpSendActionErrorCode.MISSING_PARAMS]: 'Failed to send HTTP command to {{url}}: missing params',
+	[HttpSendActionErrorCode.INVALID_PARAMS_TYPE]: 'Failed to send HTTP command: invalid params type "{{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-types/src/integrations/httpSend/timeline.ts`
around lines 41 - 61, The default messages in HttpSendActionErrorMessages ignore
the available interpolation values declared in HttpSendActionErrorContextMap for
HttpSendActionErrorCode.INVALID_TYPE, MISSING_PARAMS and INVALID_PARAMS_TYPE, so
either update the message templates in HttpSendActionErrorMessages to include
the corresponding placeholders (e.g. include {{type}} for INVALID_TYPE, {{url}}
for MISSING_PARAMS and {{paramsType}} for INVALID_PARAMS_TYPE) or remove those
entries from HttpSendActionErrorContextMap to keep the contract consistent;
locate the two symbols HttpSendActionErrorMessages and
HttpSendActionErrorContextMap and make the change so messages and context keys
match.
🤖 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/src/integrations/httpSend/index.ts`:
- Line 101: The lint rule requires typed Object.values calls; update both
occurrences that check TimelineContentTypeHTTP to use a generic type argument,
e.g. replace Object.values(TimelineContentTypeHTTP).includes(...) with
Object.values<TimelineContentTypeHTTP>(TimelineContentTypeHTTP).includes(...)
(apply the same change for the second call around the other check) so the
returned array is correctly typed.

---

Nitpick comments:
In
`@packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts`:
- Around line 41-61: The default messages in HttpSendActionErrorMessages ignore
the available interpolation values declared in HttpSendActionErrorContextMap for
HttpSendActionErrorCode.INVALID_TYPE, MISSING_PARAMS and INVALID_PARAMS_TYPE, so
either update the message templates in HttpSendActionErrorMessages to include
the corresponding placeholders (e.g. include {{type}} for INVALID_TYPE, {{url}}
for MISSING_PARAMS and {{paramsType}} for INVALID_PARAMS_TYPE) or remove those
entries from HttpSendActionErrorContextMap to keep the contract consistent;
locate the two symbols HttpSendActionErrorMessages and
HttpSendActionErrorContextMap and make the change so messages and context keys
match.
🪄 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: f62d900b-5868-44a0-9e5d-a5c8df6fcce9

📥 Commits

Reviewing files that changed from the base of the PR and between c4c70af and 911fed7.

⛔ Files ignored due to path filters (2)
  • 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
📒 Files selected for processing (5)
  • packages/timeline-state-resolver-types/src/actions.ts
  • packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.ts
  • packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts
  • packages/timeline-state-resolver/src/integrations/casparCG/index.ts
  • packages/timeline-state-resolver/src/integrations/httpSend/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/timeline-state-resolver-types/src/actions.ts
  • packages/timeline-state-resolver/src/integrations/casparCG/index.ts

Comment thread packages/timeline-state-resolver/src/integrations/httpSend/index.ts Outdated
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from 911fed7 to f7bbc06 Compare June 2, 2026 13:10
- Guard was checking cmd.type instead of cmd.paramsType, so valid paramsType
  values could be incorrectly rejected
- Also switched from the `in` operator (checks enum keys) to
  Object.values().includes() (checks enum values), matching the pattern used
  for the cmd.type validation above it
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from f7bbc06 to 47392d5 Compare June 2, 2026 13:18
@sonarqubecloud

sonarqubecloud Bot commented Jun 2, 2026

Copy link
Copy Markdown

@rjmunro rjmunro merged commit c0882da into Sofie-Automation:main Jun 2, 2026
26 checks passed
@rjmunro rjmunro deleted the rjmunro/improve-device-error-notifications branch June 2, 2026 13:39
rjmunro added a commit to bbc/sofie-core that referenced this pull request Jun 2, 2026
Updates to 10.0.0-nightly-main-20260602-133931-c0882da4d.0 which includes
the new code and context fields on ActionExecutionResult (merged via
Sofie-Automation/sofie-timeline-state-resolver#459).
rjmunro added a commit to bbc/sofie-core that referenced this pull request Jun 18, 2026
Updates to 10.0.0-nightly-main-20260602-133931-c0882da4d.0 which includes
the new code and context fields on ActionExecutionResult (merged via
Sofie-Automation/sofie-timeline-state-resolver#459).
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.

3 participants