Skip to content

feat: add transport extension with interceptor pre/post hooks#292

Merged
tuxedomm merged 5 commits intomainfrom
feat/transport-extension
Apr 7, 2026
Merged

feat: add transport extension with interceptor pre/post hooks#292
tuxedomm merged 5 commits intomainfrom
feat/transport-extension

Conversation

@tuxedomm
Copy link
Copy Markdown
Collaborator

@tuxedomm tuxedomm commented Apr 7, 2026

Add extension/transport package following the same Provider pattern as credential and fileio extensions. The Interceptor interface uses a PreRoundTrip/post-closure design that guarantees built-in transport decorators (SecurityHeader, SecurityPolicy, Retry) cannot be skipped, overridden, or tampered with by extensions. The original request context is restored after PreRoundTrip to prevent context tampering.

Change-Id: I2e51ff67a0e2d8d32944a0565c2a6781110f281f

Summary

Changes

  • Change 1
  • Change 2

Test Plan

  • Unit tests pass
  • Manual local verification confirms the lark xxx command works as expected

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Added an extensible HTTP transport provider registry and optional interceptor support to allow extensions to run pre-request and post-response hooks and wrap built-in transport behavior.
  • Tests

    • Added tests covering provider registration, sequential overrides, interceptor resolution, transport wrapping, and interceptor lifecycle behaviors.

Add extension/transport package following the same Provider pattern as
credential and fileio extensions. The Interceptor interface uses a
PreRoundTrip/post-closure design that guarantees built-in transport
decorators (SecurityHeader, SecurityPolicy, Retry) cannot be skipped,
overridden, or tampered with by extensions. The original request context
is restored after PreRoundTrip to prevent context tampering.

Change-Id: I2e51ff67a0e2d8d32944a0565c2a6781110f281f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/M Single-domain feat or fix with limited business impact label Apr 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 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: 73678aef-eff7-4bdb-89d1-494917c403c8

📥 Commits

Reviewing files that changed from the base of the PR and between b0b3a6d and f6ca1bb.

📒 Files selected for processing (1)
  • internal/cmdutil/transport.go
✅ Files skipped from review due to trivial changes (1)
  • internal/cmdutil/transport.go

📝 Walkthrough

Walkthrough

Introduces a transport extension API (Provider, Interceptor), a mutex‑guarded global provider registry with Register/GetProvider, wraps default and SDK HTTP transports with an extension middleware that invokes PreRoundTrip/post hooks, and adds unit tests for registry and integration behavior.

Changes

Cohort / File(s) Summary
Extension Transport API & Registry
extension/transport/types.go, extension/transport/registry.go
Adds Provider and Interceptor interfaces and a package-level, mutex-guarded global provider with Register and GetProvider.
Registry Tests
extension/transport/registry_test.go
New tests (stubs) verifying default nil provider, registration, override behavior, and interceptor resolution.
Middleware & Integration
internal/cmdutil/transport.go, internal/cmdutil/factory_default.go
Adds extensionMiddleware RoundTripper and wrapWithExtension to conditionally wrap transports; integrates wrapper into client construction.
Integration Tests
internal/cmdutil/factory_default_test.go
New tests verifying PreRoundTrip runs before built-in transport, context restoration, header mutation interactions with security header transport, and transport chain shape with/without a registered provider.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Registry as "exttransport.Registry"
    participant Provider
    participant Interceptor
    participant BaseTransport as "base RoundTripper"

    Client->>Registry: GetProvider()
    Registry-->>Client: provider or nil
    alt provider exists
        Client->>Provider: ResolveInterceptor(ctx)
        Provider-->>Client: interceptor or nil
    end
    alt interceptor exists
        Client->>Interceptor: PreRoundTrip(req)
        Interceptor-->>Client: postHook
        Client->>Client: restore original req.Context()
        Client->>BaseTransport: RoundTrip(req)
        BaseTransport-->>Client: resp, err
        Client->>Interceptor: postHook(resp, err)
    else
        Client->>BaseTransport: RoundTrip(req)
        BaseTransport-->>Client: resp, err
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I found a tiny registry beneath the log,
Providers line up, ready to jog.
Pre-hooks pat requests, then post-hooks cheer,
Responses hop back, both far and near.
A rabbit nods — extensions are here! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% 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 context and design rationale but is incomplete: it lacks a 'Changes' section with bullet points listing main changes and a 'Test Plan' section with specific verification steps. Complete the 'Changes' section with bullet points summarizing the main changes and fill in the 'Test Plan' section with specific test verification steps.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a transport extension with interceptor pre/post hooks, which is the primary objective of the PR.

✏️ 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 feat/transport-extension

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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

Verify execution order (Pre/Post hooks called), security header
override protection, custom header passthrough, and context tamper
prevention.

Change-Id: I8d126d777903e967bb5b9a1f3a07ba125f072ec6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@f6ca1bbff41e7c724b9242e98d59b5094dc1f110

🧩 Skill update

npx skills add larksuite/cli#feat/transport-extension -y -g

Change-Id: I8c2265ef6bfbf6a7149df2b92db9fae2e1700c1c
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

🧹 Nitpick comments (3)
extension/transport/registry.go (1)

8-28: Consider sync.RWMutex for better read concurrency.

The registry is typically written once during init() and read many times during transport construction. Using sync.RWMutex with RLock() for GetProvider() would allow concurrent reads without blocking.

However, since wrapWithExtension is called within sync.OnceValues (transport is cached), this is unlikely to be a performance concern in practice.

♻️ Optional: Use RWMutex for read-heavy access pattern
 var (
-	mu       sync.Mutex
+	mu       sync.RWMutex
 	provider Provider
 )

 func Register(p Provider) {
 	mu.Lock()
 	defer mu.Unlock()
 	provider = p
 }

 func GetProvider() Provider {
-	mu.Lock()
-	defer mu.Unlock()
+	mu.RLock()
+	defer mu.RUnlock()
 	return provider
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/transport/registry.go` around lines 8 - 28, The registry uses a
sync.Mutex (mu) which serializes all access; change mu to a sync.RWMutex, keep
using mu.Lock()/mu.Unlock() in Register(provider) for writes, and switch
GetProvider() to use mu.RLock()/mu.RUnlock() so multiple readers can
concurrently call GetProvider() while still protecting the shared variable
provider.
extension/transport/registry_test.go (1)

25-33: Extract repeated reset logic into a test helper.

Each test manually resets the global provider under lock. Consider extracting this to a helper function for cleaner tests and to ensure consistent reset behavior.

♻️ Optional: Add reset helper function
+func resetProvider(t *testing.T) {
+	t.Helper()
+	mu.Lock()
+	provider = nil
+	mu.Unlock()
+}
+
 func TestGetProvider_NilByDefault(t *testing.T) {
-	mu.Lock()
-	provider = nil
-	mu.Unlock()
+	resetProvider(t)

 	if got := GetProvider(); got != nil {
 		t.Fatalf("expected nil, got %v", got)
 	}
 }

Apply similarly to other test functions.

Also applies to: 35-47, 49-63, 65-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/transport/registry_test.go` around lines 25 - 33, Extract the
repeated locked reset logic into a test helper (e.g., resetProvider or
withResetProvider) and call it from TestGetProvider_NilByDefault and the other
tests; the helper should acquire mu, set provider = nil, and release mu to
ensure consistent teardown before each test, then replace the inline
mu.Lock()/provider = nil/mu.Unlock() blocks in tests that reference provider,
mu, and GetProvider with calls to that helper.
internal/cmdutil/factory_default_test.go (1)

110-124: Existing test relies on implicit nil state.

TestBuildSDKTransport_IncludesRetryTransport doesn't explicitly reset the extension provider, relying on the default nil state. While t.Cleanup in other tests should restore this, it's safer to explicitly set the expected state at the start of each test for robustness.

♻️ Add explicit state reset for robustness
 func TestBuildSDKTransport_IncludesRetryTransport(t *testing.T) {
+	exttransport.Register(nil) // ensure no extension is registered
+
 	transport := buildSDKTransport()

 	sec, ok := transport.(*internalauth.SecurityPolicyTransport)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/factory_default_test.go` around lines 110 - 124,
TestBuildSDKTransport_IncludesRetryTransport relies on an implicit nil extension
provider; explicitly reset the extension provider to nil at the start of the
test before calling buildSDKTransport to make the test robust. Add a statement
that sets the package's extension provider to nil (using the same setter used in
other tests or by assigning the provider variable to nil) at the top of
TestBuildSDKTransport_IncludesRetryTransport and, if applicable, call t.Cleanup
to restore the previous provider after the test so buildSDKTransport runs with
the known nil state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@extension/transport/registry_test.go`:
- Around line 25-33: Extract the repeated locked reset logic into a test helper
(e.g., resetProvider or withResetProvider) and call it from
TestGetProvider_NilByDefault and the other tests; the helper should acquire mu,
set provider = nil, and release mu to ensure consistent teardown before each
test, then replace the inline mu.Lock()/provider = nil/mu.Unlock() blocks in
tests that reference provider, mu, and GetProvider with calls to that helper.

In `@extension/transport/registry.go`:
- Around line 8-28: The registry uses a sync.Mutex (mu) which serializes all
access; change mu to a sync.RWMutex, keep using mu.Lock()/mu.Unlock() in
Register(provider) for writes, and switch GetProvider() to use
mu.RLock()/mu.RUnlock() so multiple readers can concurrently call GetProvider()
while still protecting the shared variable provider.

In `@internal/cmdutil/factory_default_test.go`:
- Around line 110-124: TestBuildSDKTransport_IncludesRetryTransport relies on an
implicit nil extension provider; explicitly reset the extension provider to nil
at the start of the test before calling buildSDKTransport to make the test
robust. Add a statement that sets the package's extension provider to nil (using
the same setter used in other tests or by assigning the provider variable to
nil) at the top of TestBuildSDKTransport_IncludesRetryTransport and, if
applicable, call t.Cleanup to restore the previous provider after the test so
buildSDKTransport runs with the known nil state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d42898d-ddd7-495a-bfd2-fc8b6c2948ed

📥 Commits

Reviewing files that changed from the base of the PR and between d3d92e3 and 65075a9.

📒 Files selected for processing (6)
  • extension/transport/registry.go
  • extension/transport/registry_test.go
  • extension/transport/types.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/transport.go

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR adds an extension/transport package following the same Provider/Registry pattern as extension/credential, allowing extensions to register pre/post-request interceptors without being able to skip or tamper with the built-in transport chain. The extensionMiddleware correctly clones the request before passing it to PreRoundTrip and restores the original context afterward, and the implementation is well-tested. The only non-blocking finding is that ResolveInterceptor's ctx parameter is always called with context.Background() at setup time, which differs from the credential extension's pattern and is not documented.

Confidence Score: 5/5

Safe to merge; the previous P1 (request not cloned before PreRoundTrip) has been resolved, and all remaining findings are non-blocking P2 suggestions.

All findings are P2 or lower — a documentation gap about context.Background() semantics in ResolveInterceptor. No logic errors, security issues, or correctness problems remain.

extension/transport/types.go warrants a minor godoc addition; all other files look correct.

Important Files Changed

Filename Overview
extension/transport/types.go New Provider/Interceptor interfaces; consistent with existing extension patterns; ctx semantics for ResolveInterceptor are undocumented (P2)
extension/transport/registry.go Mutex-protected single-provider registry; matches extension/credential pattern exactly
extension/transport/registry_test.go Covers nil-by-default, register/get, last-registration-wins, and non-nil interceptor resolution
internal/cmdutil/transport.go extensionMiddleware correctly clones request before PreRoundTrip and restores original context; wrapWithExtension uses context.Background() intentionally at setup time
internal/cmdutil/factory_default.go wrapWithExtension correctly wired into both HTTP client and SDK transport chains inside sync.OnceValues
internal/cmdutil/factory_default_test.go Thorough lifecycle tests covering execution order, context tamper prevention, and chain shape with/without extension

Sequence Diagram

sequenceDiagram
    participant Caller
    participant EM as extensionMiddleware
    participant EI as Extension Interceptor
    participant SH as SecurityHeaderTransport
    participant RT as RetryTransport
    participant SRV as HTTP Server

    Caller->>EM: RoundTrip(req)
    EM->>EM: Clone req, save origCtx
    EM->>EI: PreRoundTrip(clonedReq)
    Note over EI: May set custom headers,\nstart trace span
    EI-->>EM: post callback (or nil)
    EM->>EM: req.WithContext(origCtx)\n(context tamper prevented)
    EM->>SH: RoundTrip(req)
    SH->>RT: RoundTrip(req + security headers)
    RT->>SRV: HTTP request (with retry)
    SRV-->>RT: response
    RT-->>SH: response
    SH-->>EM: response
    EM->>EI: post(resp, err)\n(logging, span end, metrics)
    EM-->>Caller: resp, err
Loading

Reviews (3): Last reviewed commit: "style: fix gofmt formatting in transport..." | Re-trigger Greptile

Comment thread internal/cmdutil/transport.go
tuxedomm and others added 2 commits April 7, 2026 18:00
Change-Id: Iafa17d03f0ffad0830b92882e69147eba8249dd3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change-Id: If060b8d27fee9563a9c62ddbc67ed60021e55364
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cmdutil/factory_default_test.go`:
- Around line 246-248: Before installing the stub, capture the current global
provider and restore it in cleanup instead of always registering nil: call a
read/get of the current provider (or capture the return value if
exttransport.Register returns the previous provider) before calling
exttransport.Register(&stubTransportProvider{interceptor: ic}), and in t.Cleanup
use exttransport.Register(previousProvider) to restore that captured value;
update the test occurrences that call
exttransport.Register(&stubTransportProvider{...}) followed by t.Cleanup(func()
{ exttransport.Register(nil) }) (including the other spots mentioned) to use
this capture-and-restore pattern so tests do not clobber pre-existing global
state.

In `@internal/cmdutil/transport.go`:
- Line 119: The file has a formatting issue at the line containing the request
clone call (req = req.Clone(origCtx)) that fails gofmt; run gofmt (or goimports)
over internal/cmdutil/transport.go to fix whitespace/formatting so the line and
surrounding code conform to gofmt output, then re-run lint/CI and commit the
formatted file.
🪄 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: c2913ee2-da83-4157-a46b-5c71cf1880e2

📥 Commits

Reviewing files that changed from the base of the PR and between 65075a9 and b0b3a6d.

📒 Files selected for processing (3)
  • extension/transport/registry_test.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/transport.go
✅ Files skipped from review due to trivial changes (1)
  • extension/transport/registry_test.go

Comment thread internal/cmdutil/factory_default_test.go
Comment thread internal/cmdutil/transport.go Outdated
@tuxedomm tuxedomm merged commit bb38ecd into main Apr 7, 2026
15 checks passed
@tuxedomm tuxedomm deleted the feat/transport-extension branch April 7, 2026 10:21
tuxedomm added a commit that referenced this pull request Apr 8, 2026
* feat: add transport extension with interceptor pre/post hooks

Add extension/transport package following the same Provider pattern as
credential and fileio extensions. The Interceptor interface uses a
PreRoundTrip/post-closure design that guarantees built-in transport
decorators (SecurityHeader, SecurityPolicy, Retry) cannot be skipped,
overridden, or tampered with by extensions. The original request context
is restored after PreRoundTrip to prevent context tampering.

Change-Id: I2e51ff67a0e2d8d32944a0565c2a6781110f281f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants