Skip to content

fix(evm): Preserve JSON-RPC error data on plugin RPC responses - #915

Merged
shahnami merged 9 commits into
mainfrom
fix/preserve-rpc-error-data
Oct 7, 2026
Merged

shahnami merged 9 commits into
mainfrom
fix/preserve-rpc-error-data

Conversation

@shahnami

@shahnami shahnami commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

When proxying raw/plugin RPC calls, Alloy's RpcError → ProviderError conversion dropped JSON-RPC error data. Failed EVM simulations (including ERC-4337 FailedOp) therefore reached plugins without a decodeable payload, forcing opaque-revert handling and blocking submission.

This change:

  • Preserves upstream error.data on ProviderError::RpcErrorCode
  • Adds optional JsonRpcError.data (omitted when absent)
  • Passes it through EVM/Stellar plugin RPC error responses

Distinct from include_revert_data (mined-tx debug_traceTransaction recovery).

Plugin pool: resolve SDK under Piscina temps

Validating the AA plugin against this Relayer branch also surfaced a separate pool-loader bug.

The plugin compiler leaves @openzeppelin/relayer-sdk external (not bundled). At runtime the worker must require() it from plugins/node_modules. When pool-executor.js is missing, Piscina compiles the worker on the fly into os.tmpdir() and Node resolves modules from that temp path—so require('@openzeppelin/relayer-sdk') fails with MODULE_NOT_FOUND even though the SDK is installed under plugins/.

Thin plugins that re-export a package (e.g. AA's export { handler } from '@openzeppelin/relayer-plugin-aa') hit this path reliably. In-repo examples often still “work” when a prebuilt plugins/lib/pool-executor.js is present, because then require walks up into plugins/node_modules.

Fix: pool-executor passes a createRequire rooted at plugins/package.json (and a real plugins/ __dirname) into the plugin factory, so externalized packages resolve correctly regardless of the worker file location.

Testing Process

  • Unit: From<RpcError> keeps/drops data correctly
  • Unit: create_error_response_with_data serializes data and omits it when None
  • Unit: EVM rpc returns provider RpcErrorCode.data on the JSON-RPC error
  • Live Relayer (fix/preserve-rpc-error-data) + anvil: plugin pool loads AA; /plugins/aa/call/health OK; expired paymaster submit → SIMULATION_FAILED / AA32 (not OPAQUE_REVERT)

Checklist

  • Add a reference to related issues in the PR description.
  • Add unit tests if applicable.

Note

If you are using Relayer in your stack, consider adding your team or organization to our list of Relayer Users in the Wild!

Alloy's RpcError→ProviderError mapping dropped error.data, so failed
simulations (e.g. ERC-4337 FailedOp) reached plugins as opaque reverts.
Pass the upstream data field through to JsonRpcError.
@shahnami
shahnami requested a review from a team as a code owner October 7, 2026 10:00
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: f9a3a520-d384-4b73-8c89-78c3d8b0a3d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 1e43e89e-d85c-4355-9df8-63e88304b700
📥 Commits

Reviewing files that changed from the base of the PR and between fe0bea6 and 11f3d7f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • CHANGELOG.md
  • Cargo.toml
  • openapi.json
  • src/domain/relayer/evm/evm_relayer.rs
  • src/domain/relayer/evm/rpc_utils.rs
  • src/domain/relayer/stellar/stellar_relayer.rs
  • src/models/rpc/json_rpc.rs
  • src/services/gas/fetchers/polygon_zkevm.rs
  • src/services/gas/handlers/polygon_zkevm.rs
  • src/services/provider/evm/mod.rs
  • src/services/provider/mod.rs
  • src/services/provider/retry.rs
  • src/services/provider/stellar/mod.rs
  • src/utils/error_sanitization.rs
  • typos.toml

Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.


Walkthrough

Provider error conversions now retain optional upstream JSON-RPC error data. EVM and Stellar relayers pass that data into error responses when present. The JSON-RPC error model and OpenAPI schema describe the optional field.

Changes

JSON-RPC Error Data

Layer / File(s) Summary
Capture provider error data
Cargo.toml, src/services/provider/mod.rs, src/services/provider/stellar/mod.rs, src/services/provider/evm/mod.rs, src/services/provider/retry.rs, src/services/gas/..., src/utils/error_sanitization.rs
ProviderError::RpcErrorCode stores optional JSON data parsed from upstream errors. EVM and Stellar error conversions preserve that data. Retry classification continues to use the error code and message. Tests and fixtures account for the new field.
Return error data in relayer responses
src/models/rpc/json_rpc.rs, src/domain/relayer/evm/..., src/domain/relayer/stellar/stellar_relayer.rs, openapi.json, CHANGELOG.md, typos.toml
The JSON-RPC error model supports optional data and omits the field when absent. EVM and Stellar relayers include provider error data in responses. Tests and the OpenAPI schema cover the field; the changelog records the change.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Provider as Upstream JSON-RPC provider
  participant Conversion as Provider error conversion
  participant Relayer as EVM relayer
  participant Helper as create_error_response_with_data
  participant Response as JsonRpcResponse
  Provider->>Conversion: JSON-RPC error with optional data
  Conversion->>Relayer: ProviderError::RpcErrorCode
  Relayer->>Helper: error details and optional data
  Helper->>Response: JsonRpcError with optional data
Loading

Suggested reviewers: zeljkox, tirumerla

Merge Risk: ⚪ Minimal · up to 11f3d

The change appears mergeable after normal checks; no actionable failure in JSON-RPC error-data forwarding is established.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.59% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 11 files. (4 skipped: 4…
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.
Title check ✅ Passed The title clearly summarizes the main change: preserving JSON-RPC error data on plugin RPC responses.
Description check ✅ Passed The description includes the required Summary, Testing Process, and Checklist sections, and gives clear technical context and test results. The related-issues checklist item remains unchecked, but the…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit hops through errors bright
And finds the data tucked from sight
It carries payloads to the stream
Where JSON fields now hold their gleam
Then nibbles clover, pleased and neat

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

Restore original CHANGELOG wording and exclude it from typos so
pre-commit does not rewrite past release notes.
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.68786% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/domain/relayer/evm/rpc_utils.rs 93.65% 4 Missing ⚠️
src/services/provider/mod.rs 97.40% 2 Missing ⚠️
src/services/provider/stellar/mod.rs 98.14% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Exercise Stellar Call/raw-path mapping and ProviderError serialization
so patch coverage includes the new RpcErrorCode.data field.
Piscina may run pool-executor from a temp path, so ambient require cannot
see plugins/node_modules for compiler-externalized @openzeppelin/relayer-sdk.
@shahnami
shahnami requested a balanced review from Copilot October 7, 2026 11:58
Regression for compiler-externalized @openzeppelin/relayer-sdk resolving
via pluginsRequire when executePlugin runs the plugin factory.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The plugin loader and Stellar response boundary need regression coverage, and the changelog exclusion is overly broad.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Preserves upstream JSON-RPC error data for plugin RPC responses and fixes SDK resolution in temporary Piscina workers.

Changes:

  • Propagates JSON-RPC data through EVM and Stellar provider errors.
  • Extends response models, OpenAPI schema, dependencies, and tests.
  • Resolves externalized plugin SDK packages from plugins/node_modules.
File Description
Cargo.toml Enables JSON-RPC support and test dependency.
Cargo.lock Locks dependency updates.
CHANGELOG.md Documents the RPC-data fix.
openapi.json Adds optional error data schema.
plugins/​lib/​pool-executor.ts Fixes external package resolution.
src/​domain/​relayer/​evm/​evm_relayer.rs Returns EVM provider error data.
src/​domain/​relayer/​evm/​rpc_utils.rs Builds responses with optional data.
src/​domain/​relayer/​stellar/​stellar_relayer.rs Returns Stellar provider error data.
src/​models/​rpc/​json_rpc.rs Adds the error data field.
src/​services/​gas/​fetchers/​polygon_zkevm.rs Updates error construction.
src/​services/​gas/​handlers/​polygon_zkevm.rs Updates error construction.
src/​services/​provider/​evm/​mod.rs Updates EVM provider tests.
src/​services/​provider/​mod.rs Preserves Alloy RPC error data.
src/​services/​provider/​retry.rs Updates retry tests.
src/​services/​provider/​stellar/​mod.rs Preserves Stellar RPC error data.
src/​utils/​error_sanitization.rs Updates sanitization tests.
typos.toml Excludes the changelog from typo checks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread plugins/lib/pool-executor.ts
Comment thread src/domain/relayer/stellar/stellar_relayer.rs
Comment thread typos.toml Outdated
Add Stellar relayer rpc data preservation coverage, exercise executePlugin
require for the SDK, and allowlist legacy CHANGELOG typos instead of
excluding the whole file.
Bump Node 20.20, piscina, axios, rustls, quinn-proto, and hickory-resolver.
Ignore AWS-transitive rustls-webpki and aws-smithy-json advisories that need
MSRV 1.94+ or rustls 0.22+ to remediate.

@zeljkoX zeljkoX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, the core fix looks good. Keeping error.data on RpcErrorCode and passing it through is the right approach, and the anvil result (AA32 instead of OPAQUE_REVERT) confirms it works. A few things I'd like addressed before merge:

1. The SDK-resolution test doesn't catch the regression

pool-executor-sdk-require.test.ts passes even without the fix. Jest runs with cwd plugins/, and pool-executor is imported from plugins/lib/, so the old ambient require already reaches plugins/node_modules by walking up the tree. The test also can't tell cwd/plugins/package.json apart from cwd/package.json: under Jest the root is plugins/plugins/package.json, which doesn't exist and only resolves by walking up.

pluginsRequire is created when the worker module loads, so changing cwd after an in-process import does not affect it. For a real regression test, compile the worker into os.tmpdir() the way compileExecutorOnTheFly does, and spawn that file with cwd set to the parent of plugins/ (the layout the relayer actually uses). Alternatively, let the plugins root be passed in, so the test can set it explicitly. The first test only checks how Node resolves modules, not our code, and it could break on machines with a node_modules above the temp dir. I'd drop it.

2. Stellar forwards "data": null

In json_rpc_error_to_provider_error, error.get("data").cloned() keeps JSON null as Some(Value::Null), and skip_serializing_if = "Option::is_none" doesn't skip that, so clients get "data": null. The Alloy and jsonrpsee paths parse into an Option, which already turns null into None. Suggest:

data: error.get("data").filter(|v| !v.is_null()).cloned(),

3. data skips sanitization

map_provider_error / sanitize_error_description deliberately hide upstream messages, but data now goes out to clients unchanged. That's fine for EVM revert hex, and the endpoint requires an API key. Stellar data, though, can include diagnostic events, and some providers put free-form text there (including nested objects, not only hex). A hex-only allowlist would drop those. Could we add a size cap, or at least a comment saying the exemption is intentional?

4. Changelog

Only the EVM change is listed. Please add the Stellar passthrough and the pool-executor fix. For the latter, it's worth noting that it only affects on-the-fly compilation (missing plugins/lib/pool-executor.js). The production image builds pool-executor.js at install time, so prod wasn't hitting this.

5. typos.toml

Adding these under [default.extend-words] allows the misspellings across the whole repo. Please don't exclude CHANGELOG.md entirely. Scope the allowlist to that file:

[type.changelog]
extend-glob = ["CHANGELOG.md"]
extend-words = { intristic = "intristic", exectution = "exectution", concurency = "concurency", transfering = "transfering", persistance = "persistance" }

Nits

  • alloy gets the json-rpc feature on the main dependency only so a test can build an ErrorPayload. It's harmless (alloy-json-rpc is already pulled in via alloy-provider), but it's a prod feature flag that exists for a test.
  • jsonrpsee-types = "0.26.0" has to stay in step with the soroban client's jsonrpsee-core. A comment next to it would save someone a confusing type mismatch later.
  • Callers still get -32603 / "Internal error", with the upstream code only in description. That's fine for the AA plugin, which decodes data. A follow-up could pass the original code through when data is present.

The failing docker-scan check is the existing nodejs-20 CVE issue, not this PR.

@zeljkoX zeljkoX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving, the core fix is good. Please take a look at the comments above (test coverage, Stellar null data, changelog, typos scoping) before merging.

Make the SDK require test fail without pluginsRequire by spawning a
temp-compiled worker from the Relayer cwd, drop JSON-null and oversized
error data, scope changelog typo allows, and document Stellar/plugin notes.
@shahnami
shahnami merged commit 1d0c332 into main Oct 7, 2026
24 of 25 checks passed
@shahnami
shahnami deleted the fix/preserve-rpc-error-data branch October 7, 2026 18:45
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants