feat(subscription): use ConnectRPC for subscription/billing clients - #34
Draft
abhisek wants to merge 1 commit into
Draft
feat(subscription): use ConnectRPC for subscription/billing clients#34abhisek wants to merge 1 commit into
abhisek wants to merge 1 commit into
Conversation
The subscription and billing commands previously spoke native gRPC to the control plane, where an RPC error's status and rich ErrorDetail travel only in HTTP/2 trailers. The gRPC proxy in front of control-tower intermittently fails to forward those bodyless, trailer-only error responses, so callers saw "code = Internal desc = server closed the stream without sending trailers" instead of the real error. Successful responses were unaffected because their payload rides in the body. Switch these clients to the ConnectRPC protocol, which carries errors and their ErrorDetail in the response body, sidestepping trailer forwarding entirely. This mirrors the web app, which uses Connect and never exhibited the bug. - Add ControlPlaneCredentials to expose the resolved/refreshed token and tenant for building a Connect client. - Add connect.go: base URL resolution, an auth interceptor injecting the same authorization and x-tenant-id headers the gRPC per-RPC creds set, and a Connect-backed Service constructor. - Rewrite Service over the generated Connect clients; extract ErrorReason from connect.Error details and classify transient/deadline/not-found via connect.Code. - Update callers and tests to the Connect client and error model. Scoped to subscription/billing as a proof of concept; other commands still use gRPC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QuAcWAG8UNvgWQW38Kx5Q
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Proof of concept for the intermittent
code = Internal desc = server closed the stream without sending trailerserror seen on subscription/billing commands.Root cause (not the gRPC SDK): over the native gRPC protocol, an RPC error's status and rich
ErrorDetailtravel only in HTTP/2 trailers, and the error response is bodyless. The gRPC proxy in front of control-tower intermittently fails to forward those bodyless, trailer-only error responses, so callers get a transport-level failure instead of the real error. Successful responses are unaffected because their payload rides in the DATA/body frame — which is exactly why this was only ever observed on error cases, and never in the web app (which already uses ConnectRPC).Fix in this PoC: switch the subscription/billing clients to the ConnectRPC protocol, which carries errors and their
ErrorDetailin the response body, sidestepping trailer forwarding entirely.Changes
internal/app/app.go— addControlPlaneCredentials()to expose the resolved/refreshed token + tenant (a Connect client needs an HTTP client + base URL + headers, not agrpc.ClientConn).internal/cmd/subscription/connect.go(new) — control-plane base-URL resolution (sameSAFEDEP_CLOUD_CONTROL_ADDR/cloud.safedep.io:443/ insecure-transport env logic), an auth interceptor injecting the identicalauthorization+x-tenant-idheaders the gRPC per-RPC creds set, and the Connect-backedServiceconstructor.internal/cmd/subscription/service.go—Servicenow wraps the generated Connect clients; requests go throughconnect.NewRequest, responses readres.Msg,errorReasonextractsErrorDetailfromconnect.Error.Details().internal/cmd/subscription/flow.go— transient / deadline classification moved toconnect.CodeOf/connect.Code*.Scope
Intentionally limited to subscription/billing so it can be A/B'd against the gRPC path. Other commands still use gRPC, so the two coexist.
How to validate
Run
safedep subscription ondemand enable --accept-termsagainst an environment where it was flapping:main(gRPC): intermittentInternal … server closed the stream without sending trailers.on-demand billing needs a paid plan…every time, because theErrorDetailarrives in the body like a success payload.Verification
go build ./...,go vet ./internal/...,go test ./...all pass;gofmtclean; binary builds and the command tree wires.golangci-lintwas not run in the authoring environment (its binary is built with go1.25 vs the repo's go1.26.2 target); worth a local/CI lint pass.🤖 Generated with Claude Code
Generated by Claude Code