Skip to content

[#105] rpc: replace goridge with Connect-RPC over HTTP/2 (typed handlers)#105

Merged
rustatian merged 7 commits into
masterfrom
feature/connectrpc-update
May 9, 2026
Merged

[#105] rpc: replace goridge with Connect-RPC over HTTP/2 (typed handlers)#105
rustatian merged 7 commits into
masterfrom
feature/connectrpc-update

Conversation

@rustatian
Copy link
Copy Markdown
Member

@rustatian rustatian commented May 9, 2026

Summary

  • Replaces the goridge wire protocol with a Connect-RPC server (connect-go) on HTTP/2 + h2c. The same listener speaks gRPC, gRPC-Web, and Connect; gRPC reflection is enabled so grpcurl lists registered services.
  • Redesigns the RPCer contract from RPC() any to RPC() (string, http.Handler) — exactly what connect.NewXxxServiceHandler(impl) returns. Every plugin now owns its typed Connect handler; the rpc plugin's Serve collapses to a mux.Handle loop. No reflection, no custom codecs, no runtime initializers.
  • Hand-rolls the rpc plugin's own /rpc/Config and /rpc/Version endpoints with two connect.NewUnaryHandler calls under a /rpc/ sub-mux (using emptypb.Empty + wrapperspb.{BytesValue,StringValue}); they regenerate from a small proto once buf is wired up in this repo.
  • Adds optional TLS (tls.cert / tls.key) and request_timeout to the config; DSN format and default tcp://127.0.0.1:6001 are preserved.

Breaking changes

  • Wire incompatible with existing goridge clients (PHP / JS / Python) — they fail loudly on the first call after upgrade. Topology is intentional: replace, not coexist.
  • RPCer interface change breaks every other plugin currently returning any. Each follows up in its own PR by adding a service block to its api-go proto and switching RPC() to return foopbconnect.NewFooServiceHandler(impl). This branch is review-only; do not deploy alongside un-migrated plugins.

Summary by CodeRabbit

  • New Features

    • Request timeout configuration for RPC clients
    • Optional TLS/HTTPS support for RPC endpoints
    • gRPC service reflection available
  • Refactor

    • RPC transport migrated from TCP/goridge to HTTP/Connect-based handlers
  • Tests

    • Added gRPC reflection test coverage
  • Chores

    • CI workflow updated to run tests on PHP 8.5 and dependency declarations refreshed

Review Change Stack

rustatian added 3 commits May 9, 2026 14:42
Drops the custom net/rpc + goridge codec stack in favor of a
connectrpc.com/connect server speaking gRPC, gRPC-Web and the
Connect protocol on the same listener (HTTP/1.1 + h2c).

- bridge.go reflects on each plugin's RPC() struct and registers
  a Connect unary handler for every method matching the proto-shaped
  contract func(*A, *B) error where A, B implement proto.Message.
  Methods that do not match (resetter / metrics / informer's hybrid
  shape) are skipped with a warning; migrating them is tracked as a
  follow-up.
- plugin.go now serves an *http.Server with http.Server.Protocols
  for HTTP/1.1 and unencrypted HTTP/2, plus optional TLS. Init / Serve
  / Stop / Name / RPCer / Collects signatures are unchanged.
- rpc.go's API surface (Config, Version) moves to proto wrapper
  types (emptypb.Empty, wrapperspb.BytesValue, wrapperspb.StringValue)
  so the bridge picks them up.
- config.go gains optional TLS cert/key and a request_timeout field.
- gRPC reflection (grpcreflect v1 + v1alpha) is enabled so grpcurl can
  list services for operator debugging.
- Tests rewritten: PluginRPC.Hello uses wrapperspb.StringValue end to
  end and Plugin2 calls it via connect.NewClient over HTTP/1.1.

This is a breaking wire-protocol change; existing PHP / JS / Python
goridge clients will fail until they migrate to a Connect or gRPC
client. Listener address (default tcp://127.0.0.1:6001) and DSN
parsing are preserved for operator continuity.
…Endure

The bridge already iterates plugins by their Endure-assigned name, which is
unique by construction, so deduplicating service names from the route list
was dead weight. build() now returns the services list directly (filtered
to plugins that registered at least one route) and plugin.go feeds it
straight into the gRPC reflector.

Removes uniqueServices() and the route-string parsing it required.
…ndler)

The previous bridge.go reflected on each plugin's RPC() any return value
to find proto-shaped methods, then wired them to Connect via a runtime
type-erased generic handler with two custom codecs and a request
initializer. That whole layer existed only because we kept the goridge-
era contract.

Inverting the design: change the contract to what Connect natively
produces. RPCer.RPC() now returns (string, http.Handler) — the same
shape as connect.NewXxxServiceHandler(impl). Each plugin owns its own
typed handler; the rpc plugin's job collapses to a mount loop.

- Delete bridge.go entirely.
- RPCer.RPC() any → RPCer.RPC() (string, http.Handler).
- Plugin.Serve() iterates collected plugins and mux.Handle()s each
  (path, handler) pair. Service tokens for the gRPC reflector come from
  serviceName(path), which slices between the first two slashes.
- Plugin.RPC() delegates to newSelfHandlers in rpc.go.
- rpc.go: API struct replaced with newSelfHandlers(cfg, version) — two
  inline connect.NewUnaryHandler calls under a /rpc/ sub-mux. When buf
  is set up later, this regenerates from a tiny rpc.proto.
- tests/plugin1.go: PluginRPC struct dropped; Plugin1.RPC() returns a
  typed connect.NewUnaryHandler[wrapperspb.StringValue, ...] directly.

Net: -169 lines. No reflection, no custom codecs, no initializers.

Other plugins (kv, jobs, status, lock, service, centrifuge, resetter,
metrics, informer) currently return any from RPC() and no longer
satisfy the new interface; each will migrate in its own PR by adding a
service block to its api-go proto and using the generated Connect
handler.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a457255c-470b-403a-bf25-501c470c5847

📥 Commits

Reviewing files that changed from the base of the PR and between 90bc4e3 and f557500.

📒 Files selected for processing (4)
  • config.go
  • plugin.go
  • tests/plugin2.go
  • tests/rpc_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/rpc_test.go
  • config.go
  • tests/plugin2.go
  • plugin.go

📝 Walkthrough

Walkthrough

Refactors the RPC plugin system from net/rpc + goridge codec to HTTP-based Connect-RPC. Adds TLS and request timeout configuration to the Config struct; updates the RPCer interface to return mount paths and HTTP handlers; reimplements the Plugin server to use http.Server with optional TLS; replaces reflection-style RPC methods with Connect unary handlers; adapts test plugins to the new contract; adds gRPC reflection test; updates dependencies accordingly.

Changes

Connect-RPC Server Refactoring

Layer / File(s) Summary
Configuration & TLS Contracts
config.go
Adds TLS struct with Cert and Key fields; extends Config with RequestTimeout duration and optional TLS pointer; InitDefaults() applies 30-second timeout default; Valid() enforces both cert and key when TLS is configured.
RPC Endpoint Handlers
rpc.go
Implements newSelfHandlers() registering /rpc/Config and /rpc/Version as Connect unary handlers returning protobuf wrappers; removes prior API struct and reflection-style methods.
RPCer Interface
plugin.go
Changes RPC() from returning any to (string, http.Handler), allowing plugins to declare mount path and HTTP handler.
HTTP Server Implementation
plugin.go
Replaces *rpc.Server with *http.Server and atomic.Bool; reimplements Serve() to build http.ServeMux, mount plugin handlers at declared paths, enable gRPC reflection when applicable, and run HTTP/1.1+HTTP/2 server with conditional TLS; updates Stop() to gracefully shut down server; removes Register() method.
Test Plugin Implementations
tests/plugin1.go, tests/plugin2.go
Plugin1 returns mount path and Connect handler for /Hello endpoint accepting/returning wrapperspb.StringValue; Plugin2 migrates from Goridge TCP client to Connect HTTP client with 5-second timeout; removes PluginRPC wrapper.
Tests & Reflection
tests/rpc_test.go
Adds TestRpcReflection verifying gRPC reflection advertises expected service names ("rpc", "rpc_test.plugin1") via reflection client API.
Dependencies & Workflow
go.mod, tests/go.mod, .github/workflows/linux.yml
Adds direct requirements for connectrpc.com/connect, connectrpc.com/grpcreflect, google.golang.org/grpc, google.golang.org/protobuf; removes github.com/roadrunner-server/goridge/v4; bumps test PHP version from 8.4 to 8.5.

🐰 From net/rpc to Connect we leap,
HTTP handlers, oh so sweet,
TLS certs in config deep,
Reflection tests our promises keep!
A refactor rabbit's web complete. 🌐✨

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides a detailed summary of changes, breaking changes, and implementation details, but the PR checklist is incomplete with no items checked and no explicit confirmation of required criteria. Complete the PR checklist by checking all applicable items and confirming that signed commits, issue reference, documentation, tests, and changelog entries are included.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing goridge with Connect-RPC over HTTP/2 using typed handlers.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/connectrpc-update

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

rustatian added 3 commits May 9, 2026 15:44
Static reflection only buys grpcurl-list ergonomics; it can't describe
methods without FileDescriptors, and clients that use generated Connect
stubs don't need it. Dropping it removes the only consumer of the
serviceName helper, the services slice, and the connectrpc.com/grpcreflect
dependency.

The startup log keeps a list of mounted plugin names for operator visibility.
Reverts the previous removal of grpcreflect. Static reflection lets
operators run `grpcurl localhost:6001 list` to see which services the
rpc plugin currently mounts.

The path → service-name extraction is inlined at the only call site
(four lines) instead of pulled into a named helper.

Adds TestRpcReflection: spins up the plugin via endure with Plugin1,
opens a real gRPC client (google.golang.org/grpc + insecure), calls
ServerReflectionInfo's ListServices, and asserts both `rpc` and
`rpc_test.plugin1` are advertised.
@rustatian rustatian self-assigned this May 9, 2026
@rustatian rustatian added the enhancement New feature or request label May 9, 2026
@rustatian rustatian marked this pull request as ready for review May 9, 2026 14:13
Copilot AI review requested due to automatic review settings May 9, 2026 14:13
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (807c34c) to head (90bc4e3).

Additional details and impacted files
@@      Coverage Diff      @@
##   master   #105   +/-   ##
=============================
=============================

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the rpc plugin from the legacy goridge net/rpc wire protocol to a Connect-RPC (connect-go) HTTP server that can speak Connect, gRPC, and gRPC-Web over a single listener (h2c/HTTP2 + HTTP/1.1), and updates the test plugins and e2e tests accordingly.

Changes:

  • Redefines the RPCer contract to return (string, http.Handler) and mounts typed Connect handlers via http.ServeMux (no net/rpc reflection/codecs).
  • Adds gRPC server reflection handlers (via connectrpc.com/grpcreflect) and introduces optional TLS + request_timeout config.
  • Updates tests to use Connect clients and adds a reflection-based test to assert advertised services.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugin.go Replaces net/rpc server with http.Server + mux mounting for Connect/gRPC and adds reflection + TLS support.
rpc.go Implements the rpc plugin’s own /rpc/Config and /rpc/Version endpoints as Connect unary handlers under a sub-mux.
config.go Adds request_timeout and optional TLS config, with defaults and validation.
tests/plugin1.go Converts Plugin1 to expose a Connect unary handler via the new (path, handler) RPCer interface.
tests/plugin2.go Converts Plugin2 to call Plugin1 using a Connect client over HTTP.
tests/rpc_test.go Adds a gRPC reflection test and updates imports/deps to validate reflection behavior.
go.mod Drops goridge dependency; adds connect + grpcreflect + protobuf requirements.
go.sum Updates module checksums after dependency changes.
go.work.sum Updates workspace checksums due to dependency graph changes.
tests/go.mod Updates the tests module to use connect + grpc + protobuf.
tests/go.sum Updates test module checksums after dependency changes.
.github/workflows/linux.yml Updates the CI matrix PHP version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugin.go
if path == "" || handler == nil {
s.log.Warn("plugin returned empty rpc handler", "plugin", name)
continue
}
Comment thread plugin.go
s.server = &http.Server{
Handler: mux,
Protocols: protocols,
ReadHeaderTimeout: s.cfg.RequestTimeout,
Comment thread tests/plugin2.go Outdated
conn, err := net.Dial("tcp", "127.0.0.1:6001")
client := connect.NewClient[wrapperspb.StringValue, wrapperspb.StringValue](
http.DefaultClient,
"http://127.0.0.1:6001/rpc_test.plugin1/Hello",
Comment thread tests/rpc_test.go Outdated
Comment on lines +118 to +138
// give the http listener a moment to come up
time.Sleep(500 * time.Millisecond)

conn, err := grpc.NewClient("127.0.0.1:6001", grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
defer func() { _ = conn.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

stream, err := reflectionpb.NewServerReflectionClient(conn).ServerReflectionInfo(ctx)
require.NoError(t, err)

require.NoError(t, stream.Send(&reflectionpb.ServerReflectionRequest{
MessageRequest: &reflectionpb.ServerReflectionRequest_ListServices{ListServices: ""},
}))

resp, err := stream.Recv()
require.NoError(t, err)

listResp := resp.GetListServicesResponse()
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: 6

🤖 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 @.github/workflows/linux.yml:
- Line 20: The matrix currently includes "php: [ \"8.5\" ]" but no step uses it;
either remove the php entry from the workflow matrix to avoid misleading job
names, or add a step that uses the matrix value (e.g., add a setup-php action
that references matrix.php) so the PHP version is actually tested; locate the
matrix declaration containing "php" and update it accordingly and, if adding PHP
setup, ensure the step references matrix.php (e.g., uses: actions/setup-php or
equivalent).

In `@config.go`:
- Around line 42-50: In Config.Valid(), add a validation that rejects negative
request_timeout values by returning an error when c.RequestTimeout < 0; update
the Valid() function (on type Config) to check the RequestTimeout field and
return a clear error message like "request_timeout must be non-negative" so
configs with negative timeouts fail fast before startup.

In `@plugin.go`:
- Around line 156-161: The TLS setup branch in Serve() opens the socket
(s.listener) before calling tls.LoadX509KeyPair; on failure the listener is left
open—fix by closing s.listener before returning when tls.LoadX509KeyPair returns
an error: in the useTLS branch around tls.LoadX509KeyPair(s.cfg.TLS.Cert,
s.cfg.TLS.Key) call, call s.listener.Close() (or defer a conditional close)
prior to sending the error on errCh and returning so the bound socket is
released; adjust error handling in the Serve function to ensure s.listener is
closed on TLS setup failures.
- Around line 114-120: The loop over s.plugins calls rpcer.RPC() and then
mux.Handle(path, handler) which can panic on duplicate/conflicting patterns; add
validation before calling mux.Handle: keep a local or Server-level set (e.g.,
s.registeredRPCPaths or a local map[string]struct{}) and check the returned path
from rpcer.RPC() for emptiness and for prior registration/conflict, log an error
via s.log (instead of calling Handle) and skip or return an error if a duplicate
is detected; update the logic around the loop that iterates s.plugins so
duplicates are detected and handled safely before calling mux.Handle to avoid
startup panics.
- Around line 150-154: The server currently only sets ReadHeaderTimeout on
s.server which doesn't cap handler execution; wrap your handlers served by mux
with http.TimeoutHandler using s.cfg.RequestTimeout and also set
s.server.ReadTimeout (in addition to or instead of ReadHeaderTimeout) so both
request reading and handler execution are bounded; locate the http.Server
construction (s.server = &http.Server{ Handler: mux, Protocols: protocols,
ReadHeaderTimeout: s.cfg.RequestTimeout }) and replace/augment it to set
ReadTimeout: s.cfg.RequestTimeout and wrap mux routes (or mux itself) with
http.TimeoutHandler(timeout) so each RPC is cancelled after
s.cfg.RequestTimeout.

In `@tests/rpc_test.go`:
- Around line 118-123: Replace the fixed time.Sleep with a readiness poll that
repeatedly attempts a TCP connection to "127.0.0.1:6001" (using net.Dial or
net.DialTimeout) until success or a short deadline (e.g., 5s); on first
successful dial proceed to create the gRPC client with grpc.NewClient, otherwise
fail the test with require.NoError(t, err) after the timeout. Add the net
import, use a small backoff (e.g., 100ms) between attempts, and keep the
existing deferred conn.Close() behavior for the gRPC connection.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17ab786c-90eb-4283-98e1-1f024af912a1

📥 Commits

Reviewing files that changed from the base of the PR and between 807c34c and 90bc4e3.

⛔ Files ignored due to path filters (3)
  • go.sum is excluded by !**/*.sum
  • go.work.sum is excluded by !**/*.sum
  • tests/go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • .github/workflows/linux.yml
  • config.go
  • go.mod
  • plugin.go
  • rpc.go
  • tests/go.mod
  • tests/plugin1.go
  • tests/plugin2.go
  • tests/rpc_test.go

Comment thread .github/workflows/linux.yml
Comment thread config.go
Comment thread plugin.go
Comment thread plugin.go
Comment on lines +150 to +154
s.server = &http.Server{
Handler: mux,
Protocols: protocols,
ReadHeaderTimeout: s.cfg.RequestTimeout,
}
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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

find . -name "plugin.go" -type f

Repository: roadrunner-server/rpc

Length of output: 77


🏁 Script executed:

cat -n ./plugin.go | sed -n '140,165p'

Repository: roadrunner-server/rpc

Length of output: 825


🏁 Script executed:

rg -i "RequestTimeout" -B 5 -A 2 plugin.go | head -40

Repository: roadrunner-server/rpc

Length of output: 234


🏁 Script executed:

rg "type.*Config|struct {" plugin.go -A 20 | head -80

Repository: roadrunner-server/rpc

Length of output: 171


🏁 Script executed:

rg "RequestTimeout" -B 10 -A 10 plugin.go

Repository: roadrunner-server/rpc

Length of output: 530


🏁 Script executed:

rg "s\.cfg" plugin.go | head -10

Repository: roadrunner-server/rpc

Length of output: 352


🏁 Script executed:

rg "type.*Config\s+struct" -A 15

Repository: roadrunner-server/rpc

Length of output: 812


🏁 Script executed:

web_search
Go http.Server timeout fields documentation ReadHeaderTimeout vs ReadBodyTimeout vs Timeout

Repository: roadrunner-server/rpc

Length of output: 157


🌐 Web query:

Go http.Server ReadHeaderTimeout vs full request timeout documentation

💡 Result:

In Go’s net/http, the Server timeouts apply to different phases of an HTTP request, and the “full request timeout” is effectively the ReadHeaderTimeout plus the ReadTimeout (because the connection read deadline is set using those two values). 1) What ReadHeaderTimeout does (header read only) - ReadHeaderTimeout is the maximum duration allowed to read the request headers; net/http sets a read deadline for that header-reading window.[1] 2) What “full request timeout” means in practice - ReadTimeout is the maximum duration for reading the entire request, including the body.[2] - In the server implementation, net/http computes: - hdrDeadline = now + ReadHeaderTimeout (if set > 0) - wholeReqDeadline = now + ReadTimeout (if set > 0) - then sets the connection read deadline to hdrDeadline (initially) and later may adjust it to wholeReqDeadline after parsing headers.[1] - A key consequence is: if both ReadHeaderTimeout and ReadTimeout are set, the effective overall deadline for reading the whole request becomes aligned with ReadTimeout (and header time is accounted for via the deadline adjustment logic), so the “full request timeout” is not an independent timer that starts after headers; it’s based on those absolute deadlines within the same request handling window.[3][1] 3) If you’re looking for a separate “handler execution timeout” - net/http does not have a single generic “full request timeout” flag on the Server that only limits handler runtime. Instead: - http.TimeoutHandler limits the duration of the handler (ServeHTTP) and returns 503 if it runs longer than the configured time limit.[4] - ReadTimeout/ReadHeaderTimeout/WriteTimeout limit network I/O and reading/writing phases, not handler CPU/runtime.[2][4] 4) Related nuance: connection idle vs header timeout - ReadHeaderTimeout can also close idle connections if IdleTimeout is not set, because the read deadline that enforces ReadHeaderTimeout can cover elapsed idle time between requests.[5] Summary - Use ReadHeaderTimeout to cap time spent reading headers. - Use ReadTimeout to cap time spent reading the rest of the request body as part of the overall request read window.[2][1] - If you need a timeout for the handler code itself, use http.TimeoutHandler.[4] If you want, share the exact Go version you’re targeting and your timeout values, and I can describe the precise effective deadlines for that configuration based on the server code path.

Citations:


🏁 Script executed:

rg "TimeoutHandler" plugin.go

Repository: roadrunner-server/rpc

Length of output: 47


🏁 Script executed:

rg "mux\s*:=|ServeMux" plugin.go -A 5 -B 2

Repository: roadrunner-server/rpc

Length of output: 339


Use appropriate timeout mechanisms to enforce full request lifetime cap.

The config comment states that RequestTimeout should cap a single RPC request lifetime, but http.Server.ReadHeaderTimeout only limits the header reading phase. Handler execution and request body reading are not bounded by this timeout, allowing slow or hung RPCs to outlive the configured timeout.

To properly enforce a per-request lifetime cap, use http.TimeoutHandler to wrap handlers and set ReadTimeout on the server in addition to ReadHeaderTimeout (or instead of it, depending on the desired behavior). This ensures both the read phase and handler execution are bounded by the timeout.

🤖 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 `@plugin.go` around lines 150 - 154, The server currently only sets
ReadHeaderTimeout on s.server which doesn't cap handler execution; wrap your
handlers served by mux with http.TimeoutHandler using s.cfg.RequestTimeout and
also set s.server.ReadTimeout (in addition to or instead of ReadHeaderTimeout)
so both request reading and handler execution are bounded; locate the
http.Server construction (s.server = &http.Server{ Handler: mux, Protocols:
protocols, ReadHeaderTimeout: s.cfg.RequestTimeout }) and replace/augment it to
set ReadTimeout: s.cfg.RequestTimeout and wrap mux routes (or mux itself) with
http.TimeoutHandler(timeout) so each RPC is cancelled after
s.cfg.RequestTimeout.

Comment thread plugin.go
Comment thread tests/rpc_test.go Outdated
- plugin.go: skip plugin handlers whose path is missing a leading slash
  (avoids the http.ServeMux.Handle panic on bad input from a misbehaving
  plugin). Duplicate-path detection intentionally not added — plugin
  names are unique by Endure invariant.
- plugin.go: close the bound listener if tls.LoadX509KeyPair fails so
  the port doesn't stay occupied.
- plugin.go: set ReadTimeout in addition to ReadHeaderTimeout so
  RequestTimeout actually caps the request-read phase, not just header
  reading.
- config.go: reject negative request_timeout in Valid(), and rewrite
  the field doc to describe what RequestTimeout actually bounds (read
  phases — not handler runtime, not streaming RPCs).
- tests/plugin2.go: use the plugin1HelloPath constant instead of a
  hardcoded URL so server and client paths can't drift.
- tests/rpc_test.go: replace the 500ms readiness sleep with a
  require.Eventually TCP-dial poll to avoid CI flakes on slow runners.

Skipped review suggestions:
- linux.yml php matrix entry: pre-existing, owned by a separate commit.
- Duplicate-path validation: redundant given Endure's name-uniqueness
  guarantee.
@rustatian rustatian changed the title rpc: replace goridge with Connect-RPC over HTTP/2 (typed handlers) [#105] rpc: replace goridge with Connect-RPC over HTTP/2 (typed handlers) May 9, 2026
@rustatian rustatian merged commit 33f640b into master May 9, 2026
7 checks passed
@rustatian rustatian deleted the feature/connectrpc-update branch May 9, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants