Skip to content

fix(rhai): stop disclosing Rhai internals and router function errors to clients - #10004

Open
rohan-b99 wants to merge 4 commits into
devfrom
ROUTER-2050-redact-rhai-wrapper-from-client-errors
Open

rohan-b99 wants to merge 4 commits into
devfrom
ROUTER-2050-redact-rhai-wrapper-from-client-errors

Conversation

@rohan-b99

@rohan-b99 rohan-b99 commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

What it does

A failing Rhai script used to have its error reported verbatim to the client, wrapped in the router's own text. That disclosed that the router runs Rhai, the names of the script's callbacks, and the line and position of the failure:

"rhai execution error: 'Runtime error: Invalid request (line 25, position 39)
in call to function 'process_router_request' @ 'process_router_request'
(line 6, position 29)'"

Clients now receive only a message the script author chose. A thrown string is returned as written, and a thrown object map still picks the status code and the full GraphQL response body. Everything else is replaced with the status code's reason phrase and the real error is logged at ERROR level instead, which covers:

  • Failures raised by the Rhai engine itself, such as an undefined function or a type mismatch.
  • A throw carrying only a status: throw #{ status: 400 } now reads Bad Request rather than dumping the thrown map.
  • Errors from the router's own Rhai functions, such as env::get() on an unset variable, base64::decode() or json::decode() on malformed input, or reading a header that isn't there.
  • A throw the router cannot read as a message: a value that is not a string or an object map, or a map with an unreadable field. The whole map is discarded, so any message beside the bad field goes with it.

ErrorDetails grows an internal_detail field holding the unredacted Rhai error for the logs. It is skipped by serde so a value deserialized from a script's throw can never populate it, and it is never copied into a client-facing response.

Response-stage failures are logged outside the span the request macros install, so they never recorded which stage failed. Now that the client message is redacted, the log is the only record: they carry rhai.stage, and they say map_response rather than map_request. The empty-response-stream failure is redacted and logged the same way.

Why

The engine's error text describes the script's internals, and the router's own Rhai functions describe the script's inputs. They can name the environment variables a script reads, or echo a parser error for a client-supplied header. None of that is a message the script author chose to show a client.

A router function's error used to be a plain string, the same shape as a script's own throw "...", so nothing could tell the two apart. The router's Rhai functions now raise a RouterFunctionError instead, and process_error checks for it before the thrown-string branch. That lets a script's own string reach the client while a function's error text does not. Rhai prints a custom type by its type name, so the error is swapped for its text before it is logged, and the script log_* functions do the same. to_string and to_debug are registered so ${err} shows the text in scripts.

Upgrade notes

  • Client-facing messages for a thrown string no longer include the rhai execution error: 'Runtime error: ... (line N, position M)' wrapper, only the string thrown. Status codes are unchanged.
  • A catch block that catches an error from one of the router's Rhai functions now receives an error object instead of a string. ${err} still gives the message, but type_of(err) and comparisons such as err == "..." change.

Checklist

Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.

  • PR description explains the motivation for the change and relevant context for reviewing
  • PR description links appropriate GitHub/Jira tickets (creating when necessary)
  • Changeset is included for user-facing changes
  • Changes are compatible1
  • Documentation2 completed
  • Performance impact assessed and acceptable
  • Metrics and logs are added3 and documented
  • Tests added and passing4
    • Unit tests
    • Integration tests
    • Manual tests, as necessary

Exceptions

Note any exceptions here

Notes

Footnotes

  1. It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this. ↩

  2. Configuration is an important part of many changes. Where applicable please try to document configuration examples. ↩

  3. A lot of (if not most) features benefit from built-in observability and debug-level logs. Please read this guidance on metrics best-practices. ↩

  4. Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions. ↩

@rohan-b99
rohan-b99 requested a review from a team as a code owner August 13, 2026 16:37
@apollo-librarian

apollo-librarian Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 18 changed, 0 removed
* graphos/routing/(latest)/operations/subscriptions/configuration.mdx
* graphos/routing/(latest)/configuration/hot-reload-schema.mdx
* graphos/routing/(latest)/customization/custom-binary.mdx
* graphos/routing/(latest)/customization/coprocessor/reference.mdx
* graphos/routing/(latest)/customization/rhai/index.mdx
* graphos/routing/(latest)/observability/graphos/graphos-reporting.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/selectors.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/standard-instruments.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/usage-guides/subgraph-error-inclusion.mdx
* graphos/routing/(latest)/performance/traffic-shaping.mdx
* graphos/routing/(latest)/performance/caching/response-caching/customization.mdx
* graphos/routing/(latest)/performance/caching/response-caching/faq.mdx
* graphos/routing/(latest)/performance/caching/response-caching/invalidation.mdx
* graphos/routing/(latest)/performance/caching/response-caching/observability.mdx
* graphos/routing/(latest)/performance/caching/response-caching/overview.mdx
* graphos/routing/(latest)/performance/caching/response-caching/quickstart.mdx
* graphos/routing/(latest)/security/request-limits.mdx
* graphos/routing/(latest)/_sidebar.yaml

Build ID: 9bcc0c4604f539289506dfbf
Build Logs: View logs

URL: https://www.apollographql.com/docs/deploy-preview/9bcc0c4604f539289506dfbf


⚠️ AI Style Review — 13 Issues Found

Summary

The documentation has been updated to align with the style guide across several key sections. Framing improvements focus on reader-centric language such as "your client" and "your router's logs." Product and feature references were streamlined by removing possessive phrasing like "the router's" before components. Structural elements now consistently omit punctuation for list fragments, while word and symbol usage was corrected to include Oxford commas, dictionary-valid contractions, and the avoidance of semicolons. Finally, the text-formatting section now prohibits quotes around code-font strings, and the verb tense and voice were updated to favor the active voice and present tense for more direct instructions.

Duration: 4254ms
Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

A failing Rhai script used to have its error reported verbatim to the
client, wrapped in the router's own text. That disclosed that the router
runs Rhai, the names of the script's callbacks, and the line and position
of the failure:

    "rhai execution error: 'Runtime error: Invalid request (line 25, position 39)
    in call to function 'process_router_request' @ 'process_router_request'
    (line 6, position 29)'"

Clients now receive only a message the script author chose. A thrown string
is returned as written, and a thrown object map still picks the status code
and the full GraphQL response body. Everything else is replaced with the
status code's reason phrase and the real error is logged at ERROR level
instead, which covers:

- Failures raised by the Rhai engine itself, such as an undefined function
  or a type mismatch.
- A throw carrying only a status: `throw #{ status: 400 }` now reads
  `Bad Request` rather than dumping the thrown map.
- Failures in the router's own Rhai functions that raise no message, such
  as reading a header that isn't there.
- A throw the router cannot read as a message - a value that is not a
  string or an object map, or a map with an unreadable field, which is
  discarded whole so any message beside the bad field goes with it.

`ErrorDetails` grows an `internal_detail` field holding the unredacted Rhai
error for the logs. It is skipped by serde so a value deserialized from a
script's `throw` can never populate it, and it is never copied into a
client-facing response.

Response-stage failures are logged outside the span the request macros
install, so they never recorded which stage failed. Now that the client
message is redacted, that was the missing half of the only record: they
carry `rhai.stage`, and they say `map_response` rather than `map_request`.
The empty-response-stream failure is redacted and logged the same way.

Known limitation, documented in the Rhai customization docs: a router
function that raises a message of its own still returns that message. A
router function's error is indistinguishable from a script's `throw` - both
arrive as `ErrorRuntime` carrying a string - so the discrimination is on the
value, not the origin: `engine::NO_CLIENT_MESSAGE` is redacted, anything
else is returned. `env::get()` on an unset variable and `json::decode()` on
malformed input therefore still reach the client; catch them and throw your
own error on a client-facing path.

Upgrade note: client-facing messages for a thrown string no longer include
the `rhai execution error: 'Runtime error: ... (line N, position M)'`
wrapper - only the string thrown. Nothing changes for scripts themselves: a
`catch` block receives exactly what it did before, and status codes are
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rohan-b99
rohan-b99 force-pushed the ROUTER-2050-redact-rhai-wrapper-from-client-errors branch from af8703d to 487a337 Compare August 13, 2026 16:38
@rohan-b99

Copy link
Copy Markdown
Contributor Author

/claude-review

@github-actions

Copy link
Copy Markdown
Contributor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@abernix
abernix requested review from a team and removed request for a team September 4, 2026 12:25
@rohan-b99
rohan-b99 requested a review from BrynCooke October 6, 2026 09:45

@BrynCooke BrynCooke left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we need to handle all errors and not leak details.

Comment thread docs/source/routing/customization/rhai/index.mdx Outdated
Comment thread docs/source/routing/customization/rhai/index.mdx
Comment thread apollo-router/src/plugins/rhai/engine/mod.rs Outdated
Comment thread apollo-router/src/plugins/rhai/mod.rs Outdated
Comment thread apollo-router/src/plugins/rhai/mod.rs Outdated
Comment thread .changesets/fix_rohan_b99_redact_rhai_internals_from_client_errors.md Outdated
rohan-b99 and others added 3 commits October 7, 2026 16:34
The comments around the empty-response-stream detail and the reason-phrase
fallback restated what the code and log line already say. Keep only the
log-search prefix and the 599 fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test covers errors from the router's own Rhai functions in general,
not only ones that carry no message, which the next commit makes the only
kind. Rename it and its log snapshot to match; no change to what it
asserts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The router's Rhai functions raised their errors as plain strings, the same
shape as a script's own `throw "..."`, so their text reached clients. Any
caller could learn which environment variables a script reads
(`env::get()` on an unset variable), or trigger parser errors from
`base64::decode()` and `json::decode()` on a client header.

Those functions now raise a `RouterFunctionError`. `process_error`
recognises it before the thrown-string branch: the client gets the status
code's reason phrase and the full text is logged. A thrown string that the
script chose still reaches the client unchanged.

Rhai prints a custom type by its type name, so the error is swapped for its
text before it is logged, and the script `log_*` functions do the same.
`to_string` and `to_debug` are registered so `${err}` in a script shows the
text.

This replaces `NO_CLIENT_MESSAGE`. A missing header now raises a message that
names the header, since that text no longer reaches the client.

A `catch` block that catches one of these errors now receives an error
object instead of a string. `${err}` still gives the message, but
`type_of(err)` and comparisons such as `err == "..."` change. The docs and
changeset call this out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rohan-b99 rohan-b99 changed the title fix(rhai): redact the Rhai wrapper from client-facing error responses fix(rhai): stop disclosing Rhai internals and router function errors to clients Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants