Repository: open-telemetry/opentelemetry-collector

Issue:
[configauth] The configauth.Authentication should be suffixed with Config or rename to Config.
See the rules: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/coding-guidelines.md#configuration-structs
See the code: https://github.com/open-telemetry/opentelemetry-collector/blob/main/config/configauth/configauth.go#L26 

Discussion:
@bogdandrutu: cc @mx-psi for the blocking issue for the configauth stabilization.@mx-psi: #12904 fixes this

Solution patch:
diff --git a/.chloggen/middleware-struct.yaml b/.chloggen/middleware-struct.yaml
new file mode 100644
index 000000000000..870041bd700c
--- /dev/null
+++ b/.chloggen/middleware-struct.yaml
@@ -0,0 +1,25 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
+component: configmiddleware
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add configmiddleware struct.
+
+# One or more tracking issues or pull requests related to the change
+issues: [12603, 9591]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext:
+
+# Optional: The change log or logs in which this entry should be included.
+# e.g. '[user]' or '[user, api]'
+# Include 'user' if the change is relevant to end users.
+# Include 'api' if there is a change to a library API.
+# Default: '[user]'
+change_logs: [user]
diff --git a/.github/workflows/utils/cspell.json b/.github/workflows/utils/cspell.json
index 834c8c557b68..ddae277a1554 100644
--- a/.github/workflows/utils/cspell.json
+++ b/.github/workflows/utils/cspell.json
@@ -192,6 +192,7 @@
       "extensionauthtest",
       "extensioncapabilities",
       "extensionmiddleware",
+      "extensionmiddlewaretest",
       "extensionhelper",
       "extensiontest",
       "extensionz",
@@ -269,6 +270,7 @@
       "lables",
       "lastest",
       "ldflags",
+      "limitermiddleware",
       "localhostgate",
       "loggingexporter",
       "logstest",
diff --git a/config/configmiddleware/Makefile b/config/configmiddleware/Makefile
new file mode 100644
index 000000000000..ded7a36092dc
--- /dev/null
+++ b/config/configmiddleware/Makefile
@@ -0,0 +1 @@
+include ../../Makefile.Common
diff --git a/config/configmiddleware/README.md b/config/configmiddleware/README.md
new file mode 100644
index 000000000000..2dfe728f3fb6
--- /dev/null
+++ b/config/configmiddleware/README.md
@@ -0,0 +1,37 @@
+# OpenTelemetry Collector Middleware Configuration
+
+This package implements a configuration struct for referring to
+[middleware extensions](../../extension/extensionmiddleware/README.md).
+
+## Overview
+
+The `configmiddleware` package defines a `Config` type that
+allows components to configure middleware extensions, typically as
+an ordered list.
+This support is built in for push-based receivers configured through
+`confighttp` and `configgrpc`, as for example in the OTLP receiver:
+
+```yaml
+receivers:
+  otlp:
+    protocols:
+	  http:
+	    middlewares:
+		- id: limitermiddleware
+```
+
+## Methods
+
+The package provides four key methods to retrieve appropriate middleware handlers:
+
+1. **GetHTTPClientRoundTripper**: Obtains a function to wrap an HTTP client with a middleware extension via a `http.RoundTripper`.
+
+2. **GetHTTPServerHandler**: Obtains a function to wrap an HTTP server with a middleware extension via a `http.Handler`.
+
+3. **GetGRPCClientOptions**: Obtains a `[]grpc.DialOption` that configure a middleware extension for gRPC clients.
+
+4. **GetGRPCServerOptions**: Obtains a `[]grpc.ServerOption` that configure a middleware extension for gRPC servers.
+
+These functions are typically called during Start() by a component,
+passing the `component.Host` extensions.
+An error is returned if the named extension cannot be found.
diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go
new file mode 100644
index 000000000000..de21ad844b42
--- /dev/null
+++ b/config/configmiddleware/configmiddleware.go
@@ -0,0 +1,92 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package configmiddleware implements a configuration struct to
+// name middleware extensions.
+package configmiddleware // import "go.opentelemetry.io/collector/config/configmiddleware"
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"google.golang.org/grpc"
+
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+var (
+	errMiddlewareNotFound = errors.New("middleware not found")
+	errNotHTTPServer      = errors.New("requested extension is not an HTTP server middleware")
+	errNotGRPCServer      = errors.New("requested extension is not a gRPC server middleware")
+	errNotHTTPClient      = errors.New("requested extension is not an HTTP client middleware")
+	errNotGRPCClient      = errors.New("requested extension is not a gRPC client middleware")
+)
+
+// Middleware defines the extension ID for a middleware component.
+type Config struct {
+	// ID specifies the name of the extension to use.
+	ID component.ID `mapstructure:"id,omitempty"`
+}
+
+// GetHTTPClientRoundTripper attempts to select the appropriate
+// extensionmiddleware.HTTPClient from the map of extensions, and
+// returns the HTTP client wrapper function. If a middleware is not
+// found, an error is returned.  This should only be used by HTTP
+// clients.
+func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[component.ID]component.Component) (func(http.RoundTripper) (http.RoundTripper, error), error) {
+	if ext, found := extensions[m.ID]; found {
+		if client, ok := ext.(extensionmiddleware.HTTPClient); ok {
+			return client.GetHTTPRoundTripper, nil
+		}
+		return nil, errNotHTTPClient
+	}
+	return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound)
+}
+
+// GetHTTPServerHandler attempts to select the appropriate
+// extensionmiddleware.HTTPServer from the map of extensions, and
+// returns the http.Handler wrapper function. If a middleware is not
+// found, an error is returned.  This should only be used by HTTP
+// servers.
+func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component.ID]component.Component) (func(http.Handler) (http.Handler, error), error) {
+	if ext, found := extensions[m.ID]; found {
+		if server, ok := ext.(extensionmiddleware.HTTPServer); ok {
+			return server.GetHTTPHandler, nil
+		}
+		return nil, errNotHTTPServer
+	}
+
+	return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound)
+}
+
+// GetGRPCClientOptions attempts to select the appropriate
+// extensionmiddleware.GRPCClient from the map of extensions, and
+// returns the gRPC dial options. If a middleware is not found, an
+// error is returned.  This should only be used by gRPC clients.
+func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component.ID]component.Component) ([]grpc.DialOption, error) {
+	if ext, found := extensions[m.ID]; found {
+		if client, ok := ext.(extensionmiddleware.GRPCClient); ok {
+			return client.GetGRPCClientOptions()
+		}
+		return nil, errNotGRPCClient
+	}
+	return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound)
+}
+
+// GetGRPCServerOptions attempts to select the appropriate
+// extensionmiddleware.GRPCServer from the map of extensions, and
+// returns the gRPC server options. If a middleware is not found, an
+// error is returned.  This should only be used by gRPC servers.
+func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component.ID]component.Component) ([]grpc.ServerOption, error) {
+	if ext, found := extensions[m.ID]; found {
+		if server, ok := ext.(extensionmiddleware.GRPCServer); ok {
+			return server.GetGRPCServerOptions()
+		}
+		return nil, errNotGRPCServer
+	}
+
+	return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound)
+}
diff --git a/config/configmiddleware/go.mod b/config/configmiddleware/go.mod
new file mode 100644
index 000000000000..7eb41290f22f
--- /dev/null
+++ b/config/configmiddleware/go.mod
@@ -0,0 +1,56 @@
+module go.opentelemetry.io/collector/config/configmiddleware
+
+go 1.23.0
+
+require (
+	github.com/stretchr/testify v1.10.0
+	go.opentelemetry.io/collector/component v1.30.0
+	go.opentelemetry.io/collector/extension v1.30.0
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0
+	go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.0.0-00010101000000-000000000000
+	google.golang.org/grpc v1.71.1
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/go-version v1.7.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
+	go.opentelemetry.io/collector/internal/telemetry v0.124.0 // indirect
+	go.opentelemetry.io/collector/pdata v1.30.0 // indirect
+	go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/log v0.11.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace go.opentelemetry.io/collector/component => ../../component
+
+replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
+
+replace go.opentelemetry.io/collector/pdata => ../../pdata
+
+replace go.opentelemetry.io/collector/pipeline => ../../pipeline
+
+replace go.opentelemetry.io/collector/featuregate => ../../featuregate
+
+replace go.opentelemetry.io/collector/extension => ../../extension
diff --git a/config/configmiddleware/go.sum b/config/configmiddleware/go.sum
new file mode 100644
index 000000000000..519e188048fa
--- /dev/null
+++ b/config/configmiddleware/go.sum
@@ -0,0 +1,95 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU=
+go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
+go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/extension/extensionmiddleware/README.md b/extension/extensionmiddleware/README.md
index a28aa0f88429..fd4c8747ed87 100644
--- a/extension/extensionmiddleware/README.md
+++ b/extension/extensionmiddleware/README.md
@@ -2,7 +2,8 @@
 
 This package implements interfaces for injecting middleware behavior
 in OpenTelemetry Collector exporters and receivers.  See the
-associated `configmiddleware` package for referring to middleware
+[associated `configmiddleware` package](../../config/configmiddleware/README.md)
+for referring to middleware
 extensions in component configurations.
 
 ## Overview
@@ -35,7 +36,8 @@ extension cannot be configured.
 New protocols and new ways to configure middleware can be introduced
 by adding new interfaces.  Note that for each interface, there is a
 corresponding method to locate a named middleware extension that
-satisfies the interface in the `configmiddleware` package.
+satisfies the interface in
+[the `configmiddleware` package](../../config/configmiddleware/README.md) .
 
 ### HTTP
 
diff --git a/extension/extensionmiddleware/client.go b/extension/extensionmiddleware/client.go
index 7b70bf82b0a5..fb24a18339ef 100644
--- a/extension/extensionmiddleware/client.go
+++ b/extension/extensionmiddleware/client.go
@@ -20,3 +20,27 @@ type GRPCClient interface {
 	// GetGRPCClientOptions returns the gRPC dial options to use for client connections.
 	GetGRPCClientOptions() ([]grpc.DialOption, error)
 }
+
+var _ HTTPClient = (*GetHTTPRoundTripperFunc)(nil)
+
+// GetHTTPRoundTripperFunc is a function that implements HTTPClient.
+type GetHTTPRoundTripperFunc func(base http.RoundTripper) (http.RoundTripper, error)
+
+func (f GetHTTPRoundTripperFunc) GetHTTPRoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
+	if f == nil {
+		return base, nil
+	}
+	return f(base)
+}
+
+var _ GRPCClient = (*GetGRPCClientOptionsFunc)(nil)
+
+// GetGRPCClientOptionsFunc is a function that implements GRPCClient.
+type GetGRPCClientOptionsFunc func() ([]grpc.DialOption, error)
+
+func (f GetGRPCClientOptionsFunc) GetGRPCClientOptions() ([]grpc.DialOption, error) {
+	if f == nil {
+		return nil, nil
+	}
+	return f()
+}
diff --git a/extension/extensionmiddleware/go.mod b/extension/extensionmiddleware/go.mod
index 8dba1fab9a5c..f088b0165660 100644
--- a/extension/extensionmiddleware/go.mod
+++ b/extension/extensionmiddleware/go.mod
@@ -2,12 +2,18 @@ module go.opentelemetry.io/collector/extension/extensionmiddleware
 
 go 1.23.0
 
-require google.golang.org/grpc v1.71.1
+require (
+	github.com/stretchr/testify v1.10.0
+	google.golang.org/grpc v1.71.1
+)
 
 require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/sys v0.32.0 // indirect
 	golang.org/x/text v0.24.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
 	google.golang.org/protobuf v1.36.4 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/extension/extensionmiddleware/go.sum b/extension/extensionmiddleware/go.sum
index abed198de411..17e3a338101f 100644
--- a/extension/extensionmiddleware/go.sum
+++ b/extension/extensionmiddleware/go.sum
@@ -1,3 +1,5 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -8,6 +10,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
@@ -32,3 +38,7 @@ google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
 google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
 google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/extension/extensionmiddleware/server.go b/extension/extensionmiddleware/server.go
index 46fdb105126e..6fa9ca1ae609 100644
--- a/extension/extensionmiddleware/server.go
+++ b/extension/extensionmiddleware/server.go
@@ -20,3 +20,27 @@ type GRPCServer interface {
 	// GetGRPCServerOptions returns options for a gRPC server.
 	GetGRPCServerOptions() ([]grpc.ServerOption, error)
 }
+
+var _ HTTPServer = (*GetHTTPHandlerFunc)(nil)
+
+// GetHTTPHandlerFunc is a function that implements HTTPServer.
+type GetHTTPHandlerFunc func(base http.Handler) (http.Handler, error)
+
+func (f GetHTTPHandlerFunc) GetHTTPHandler(base http.Handler) (http.Handler, error) {
+	if f == nil {
+		return base, nil
+	}
+	return f(base)
+}
+
+var _ GRPCServer = (*GetGRPCServerOptionsFunc)(nil)
+
+// GetGRPCServerOptionsFunc is a function that implements GRPCServer.
+type GetGRPCServerOptionsFunc func() ([]grpc.ServerOption, error)
+
+func (f GetGRPCServerOptionsFunc) GetGRPCServerOptions() ([]grpc.ServerOption, error) {
+	if f == nil {
+		return nil, nil
+	}
+	return f()
+}
diff --git a/versions.yaml b/versions.yaml
index 99c4e58a8633..c45665201b91 100644
--- a/versions.yaml
+++ b/versions.yaml
@@ -42,6 +42,7 @@ module-sets:
       - go.opentelemetry.io/collector/config/configgrpc
       - go.opentelemetry.io/collector/config/confighttp
       - go.opentelemetry.io/collector/config/confighttp/xconfighttp
+      - go.opentelemetry.io/collector/config/configmiddleware
       - go.opentelemetry.io/collector/config/configtelemetry
       - go.opentelemetry.io/collector/connector
       - go.opentelemetry.io/collector/connector/connectortest
@@ -62,6 +63,7 @@ module-sets:
       - go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest
       - go.opentelemetry.io/collector/extension/extensioncapabilities
       - go.opentelemetry.io/collector/extension/extensionmiddleware
+      - go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest
       - go.opentelemetry.io/collector/extension/extensiontest
       - go.opentelemetry.io/collector/extension/zpagesextension
       - go.opentelemetry.io/collector/extension/memorylimiterextension



Test patch:
diff --git a/config/configmiddleware/configmiddleware_test.go b/config/configmiddleware/configmiddleware_test.go
new file mode 100644
index 000000000000..5cdb2628fbaf
--- /dev/null
+++ b/config/configmiddleware/configmiddleware_test.go
@@ -0,0 +1,256 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package configmiddleware
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc"
+
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/extension"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
+)
+
+var testID = component.MustNewID("test")
+
+type mockWrongType struct {
+	component.StartFunc
+	component.ShutdownFunc
+}
+
+func TestConfig_GetHTTPServerHandler(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		middleware Config
+		extensions map[component.ID]component.Component
+		wantErr    error
+	}{
+		{
+			name: "found_and_valid",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: extensionmiddlewaretest.NewNop(),
+			},
+			wantErr: nil,
+		},
+		{
+			name: "middleware_not_found",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{},
+			wantErr:    errMiddlewareNotFound,
+		},
+		{
+			name: "middleware_wrong_type",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: mockWrongType{},
+			},
+			wantErr: errNotHTTPServer,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			value, err := tt.middleware.GetHTTPServerHandler(ctx, tt.extensions)
+
+			if tt.wantErr != nil {
+				require.ErrorIs(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, value)
+			}
+		})
+	}
+}
+
+func TestConfig_GetHTTPClientRoundTripper(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		middleware Config
+		extensions map[component.ID]component.Component
+		wantErr    error
+	}{
+		{
+			name: "found_and_valid",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: extensionmiddlewaretest.NewNop(),
+			},
+			wantErr: nil,
+		},
+		{
+			name: "middleware_not_found",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{},
+			wantErr:    errMiddlewareNotFound,
+		},
+		{
+			name: "middleware_wrong_type",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: mockWrongType{},
+			},
+			wantErr: errNotHTTPClient,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			value, err := tt.middleware.GetHTTPClientRoundTripper(ctx, tt.extensions)
+
+			if tt.wantErr != nil {
+				require.ErrorIs(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, value)
+			}
+		})
+	}
+}
+
+func TestConfig_GetGRPCServerOptions(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		middleware Config
+		extensions map[component.ID]component.Component
+		wantErr    error
+	}{
+		{
+			name: "found_and_valid",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: struct {
+					extension.Extension
+					extensionmiddleware.GetGRPCServerOptionsFunc
+				}{
+					Extension: extensionmiddlewaretest.NewNop(),
+					GetGRPCServerOptionsFunc: func() ([]grpc.ServerOption, error) {
+						return []grpc.ServerOption{
+							grpc.EmptyServerOption{},
+						}, nil
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			name: "middleware_not_found",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{},
+			wantErr:    errMiddlewareNotFound,
+		},
+		{
+			name: "middleware_wrong_type",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: mockWrongType{},
+			},
+			wantErr: errNotGRPCServer,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			value, err := tt.middleware.GetGRPCServerOptions(ctx, tt.extensions)
+
+			if tt.wantErr != nil {
+				require.ErrorIs(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, value)
+			}
+		})
+	}
+}
+
+func TestConfig_GetGRPCClientOptions(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		middleware Config
+		extensions map[component.ID]component.Component
+		wantErr    error
+	}{
+		{
+			name: "found_and_valid",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: struct {
+					extension.Extension
+					extensionmiddleware.GetGRPCClientOptionsFunc
+				}{
+					Extension: extensionmiddlewaretest.NewNop(),
+					GetGRPCClientOptionsFunc: func() ([]grpc.DialOption, error) {
+						return []grpc.DialOption{
+							grpc.EmptyDialOption{},
+						}, nil
+					},
+				},
+			},
+			wantErr: nil,
+		},
+		{
+			name: "middleware_not_found",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{},
+			wantErr:    errMiddlewareNotFound,
+		},
+		{
+			name: "middleware_wrong_type",
+			middleware: Config{
+				ID: testID,
+			},
+			extensions: map[component.ID]component.Component{
+				testID: mockWrongType{},
+			},
+			wantErr: errNotGRPCClient,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			value, err := tt.middleware.GetGRPCClientOptions(ctx, tt.extensions)
+
+			if tt.wantErr != nil {
+				require.ErrorIs(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, value)
+			}
+		})
+	}
+}
diff --git a/extension/extensionmiddleware/client_test.go b/extension/extensionmiddleware/client_test.go
new file mode 100644
index 000000000000..c525ffa3b763
--- /dev/null
+++ b/extension/extensionmiddleware/client_test.go
@@ -0,0 +1,114 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddleware
+
+import (
+	"errors"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc"
+)
+
+func TestGetHTTPRoundTripperFunc(t *testing.T) {
+	// Create a base round tripper for testing
+	baseRT := http.DefaultTransport
+
+	t.Run("nil function", func(t *testing.T) {
+		var nilFunc GetHTTPRoundTripperFunc
+		rt, err := nilFunc.GetHTTPRoundTripper(baseRT)
+		require.NoError(t, err)
+		require.Equal(t, baseRT, rt)
+	})
+
+	t.Run("identity function", func(t *testing.T) {
+		identityFunc := GetHTTPRoundTripperFunc(func(base http.RoundTripper) (http.RoundTripper, error) {
+			return base, nil
+		})
+		rt, err := identityFunc.GetHTTPRoundTripper(baseRT)
+		require.NoError(t, err)
+		require.Equal(t, baseRT, rt)
+	})
+
+	t.Run("wrapping function", func(t *testing.T) {
+		// Create a custom round tripper wrapper for testing
+		customRT := &testRoundTripper{base: baseRT}
+
+		wrapperFunc := GetHTTPRoundTripperFunc(func(base http.RoundTripper) (http.RoundTripper, error) {
+			return &testRoundTripper{base: base}, nil
+		})
+
+		rt, err := wrapperFunc.GetHTTPRoundTripper(baseRT)
+		require.NoError(t, err)
+		require.IsType(t, customRT, rt, "wrapper function should return the custom round tripper")
+	})
+
+	t.Run("error function", func(t *testing.T) {
+		expectedErr := errors.New("round tripper error")
+		errorFunc := GetHTTPRoundTripperFunc(func(_ http.RoundTripper) (http.RoundTripper, error) {
+			return nil, expectedErr
+		})
+
+		rt, err := errorFunc.GetHTTPRoundTripper(baseRT)
+		require.Error(t, err)
+		require.Equal(t, expectedErr, err)
+		require.Nil(t, rt)
+	})
+}
+
+// testRoundTripper is a simple round tripper implementation for testing
+type testRoundTripper struct {
+	base http.RoundTripper
+}
+
+func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Just delegate to the base round tripper
+	return t.base.RoundTrip(req)
+}
+
+func TestGetGRPCClientOptionsFunc(t *testing.T) {
+	t.Run("nil function", func(t *testing.T) {
+		var nilFunc GetGRPCClientOptionsFunc
+		options, err := nilFunc.GetGRPCClientOptions()
+		require.NoError(t, err)
+		require.Nil(t, options)
+	})
+
+	t.Run("empty options function", func(t *testing.T) {
+		emptyFunc := GetGRPCClientOptionsFunc(func() ([]grpc.DialOption, error) {
+			return []grpc.DialOption{}, nil
+		})
+
+		options, err := emptyFunc.GetGRPCClientOptions()
+		require.NoError(t, err)
+		require.Empty(t, options)
+	})
+
+	t.Run("options function", func(t *testing.T) {
+		// Create some test dial options
+		dialOpt1 := grpc.WithAuthority("test-authority")
+		dialOpt2 := grpc.WithDisableRetry()
+
+		optionsFunc := GetGRPCClientOptionsFunc(func() ([]grpc.DialOption, error) {
+			return []grpc.DialOption{dialOpt1, dialOpt2}, nil
+		})
+
+		options, err := optionsFunc.GetGRPCClientOptions()
+		require.NoError(t, err)
+		require.Len(t, options, 2)
+	})
+
+	t.Run("error function", func(t *testing.T) {
+		expectedErr := errors.New("grpc options error")
+		errorFunc := GetGRPCClientOptionsFunc(func() ([]grpc.DialOption, error) {
+			return nil, expectedErr
+		})
+
+		options, err := errorFunc.GetGRPCClientOptions()
+		require.Error(t, err)
+		require.Equal(t, expectedErr, err)
+		require.Nil(t, options)
+	})
+}
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/Makefile b/extension/extensionmiddleware/extensionmiddlewaretest/Makefile
new file mode 100644
index 000000000000..bdd863a203be
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/Makefile
@@ -0,0 +1 @@
+include ../../../Makefile.Common
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/err.go b/extension/extensionmiddleware/extensionmiddlewaretest/err.go
new file mode 100644
index 000000000000..b881dae2c519
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/err.go
@@ -0,0 +1,50 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddlewaretest // import "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
+
+import (
+	"net/http"
+
+	"google.golang.org/grpc"
+
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/extension"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+var (
+	_ extension.Extension            = (*baseExtension)(nil)
+	_ extensionmiddleware.HTTPClient = (*baseExtension)(nil)
+	_ extensionmiddleware.GRPCClient = (*baseExtension)(nil)
+	_ extensionmiddleware.HTTPServer = (*baseExtension)(nil)
+	_ extensionmiddleware.GRPCServer = (*baseExtension)(nil)
+)
+
+type baseExtension struct {
+	component.StartFunc
+	component.ShutdownFunc
+	extensionmiddleware.GetHTTPHandlerFunc
+	extensionmiddleware.GetGRPCServerOptionsFunc
+	extensionmiddleware.GetHTTPRoundTripperFunc
+	extensionmiddleware.GetGRPCClientOptionsFunc
+}
+
+// NewErr returns a new [extension.Extension] that implements all
+// extensionmiddleware interface and always returns an error.
+func NewErr(err error) extension.Extension {
+	return &baseExtension{
+		GetHTTPRoundTripperFunc: func(http.RoundTripper) (http.RoundTripper, error) {
+			return nil, err
+		},
+		GetGRPCClientOptionsFunc: func() ([]grpc.DialOption, error) {
+			return nil, err
+		},
+		GetHTTPHandlerFunc: func(http.Handler) (http.Handler, error) {
+			return nil, err
+		},
+		GetGRPCServerOptionsFunc: func() ([]grpc.ServerOption, error) {
+			return nil, err
+		},
+	}
+}
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/err_test.go b/extension/extensionmiddleware/extensionmiddlewaretest/err_test.go
new file mode 100644
index 000000000000..14bcf291b5b0
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/err_test.go
@@ -0,0 +1,41 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddlewaretest
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+func TestErrClient(t *testing.T) {
+	client := NewErr(errors.New("error"))
+
+	httpClient, ok := client.(extensionmiddleware.HTTPClient)
+	require.True(t, ok)
+	_, err := httpClient.GetHTTPRoundTripper(nil)
+	require.Error(t, err)
+
+	grpcClient, ok := client.(extensionmiddleware.GRPCClient)
+	require.True(t, ok)
+	_, err = grpcClient.GetGRPCClientOptions()
+	require.Error(t, err)
+}
+
+func TestErrServer(t *testing.T) {
+	server := NewErr(errors.New("error"))
+
+	httpServer, ok := server.(extensionmiddleware.HTTPServer)
+	require.True(t, ok)
+	_, err := httpServer.GetHTTPHandler(nil)
+	require.Error(t, err)
+
+	grpcServer, ok := server.(extensionmiddleware.GRPCServer)
+	require.True(t, ok)
+	_, err = grpcServer.GetGRPCServerOptions()
+	require.Error(t, err)
+}
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/go.mod b/extension/extensionmiddleware/extensionmiddlewaretest/go.mod
new file mode 100644
index 000000000000..802ca6673ee8
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/go.mod
@@ -0,0 +1,53 @@
+module go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest
+
+go 1.23.0
+
+require (
+	github.com/stretchr/testify v1.10.0
+	go.opentelemetry.io/collector/component v1.30.0
+	go.opentelemetry.io/collector/extension v1.30.0
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0
+	google.golang.org/grpc v1.71.1
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/go-version v1.7.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
+	go.opentelemetry.io/collector/internal/telemetry v0.124.0 // indirect
+	go.opentelemetry.io/collector/pdata v1.30.0 // indirect
+	go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/log v0.11.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ..
+
+replace go.opentelemetry.io/collector/component => ../../../component
+
+replace go.opentelemetry.io/collector/pdata => ../../../pdata
+
+replace go.opentelemetry.io/collector/extension => ../..
+
+replace go.opentelemetry.io/collector/internal/telemetry => ../../../internal/telemetry
+
+replace go.opentelemetry.io/collector/pipeline => ../../../pipeline
+
+replace go.opentelemetry.io/collector/featuregate => ../../../featuregate
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/go.sum b/extension/extensionmiddleware/extensionmiddlewaretest/go.sum
new file mode 100644
index 000000000000..519e188048fa
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/go.sum
@@ -0,0 +1,95 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU=
+go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
+go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/nop.go b/extension/extensionmiddleware/extensionmiddlewaretest/nop.go
new file mode 100644
index 000000000000..7d8415577cf3
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/nop.go
@@ -0,0 +1,16 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddlewaretest // import "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
+
+import (
+	"go.opentelemetry.io/collector/extension"
+)
+
+// NewNop returns a new [extension.Extension] that implements
+// the all the extensionmiddleware interfaces.  For HTTP requests it
+// returns the base RoundTripper and for gRPC requests it returns an
+// empty slice of options.
+func NewNop() extension.Extension {
+	return &baseExtension{}
+}
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go b/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go
new file mode 100644
index 000000000000..4b136369f75e
--- /dev/null
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go
@@ -0,0 +1,44 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddlewaretest
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+func TestNopClient(t *testing.T) {
+	client := NewNop()
+
+	httpClient, ok := client.(extensionmiddleware.HTTPClient)
+	require.True(t, ok)
+	rt, err := httpClient.GetHTTPRoundTripper(nil)
+	require.NoError(t, err)
+	require.Nil(t, rt)
+
+	grpcClient, ok := client.(extensionmiddleware.GRPCClient)
+	require.True(t, ok)
+	grpcOpts, err := grpcClient.GetGRPCClientOptions()
+	require.NoError(t, err)
+	require.Nil(t, grpcOpts)
+}
+
+func TestNopServer(t *testing.T) {
+	client := NewNop()
+
+	httpServer, ok := client.(extensionmiddleware.HTTPServer)
+	require.True(t, ok)
+	rt, err := httpServer.GetHTTPHandler(nil)
+	require.NoError(t, err)
+	require.Nil(t, rt)
+
+	grpcServer, ok := client.(extensionmiddleware.GRPCServer)
+	require.True(t, ok)
+	grpcOpts, err := grpcServer.GetGRPCServerOptions()
+	require.NoError(t, err)
+	require.Nil(t, grpcOpts)
+}
diff --git a/extension/extensionmiddleware/server_test.go b/extension/extensionmiddleware/server_test.go
new file mode 100644
index 000000000000..e5b00cb7f1cf
--- /dev/null
+++ b/extension/extensionmiddleware/server_test.go
@@ -0,0 +1,106 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensionmiddleware
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc"
+)
+
+func TestGetHTTPHandlerFunc(t *testing.T) {
+	t.Run("nil_function", func(t *testing.T) {
+		var f GetHTTPHandlerFunc
+		baseHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(http.StatusNoContent)
+		})
+
+		handler, err := f.GetHTTPHandler(baseHandler)
+		require.NoError(t, err)
+
+		rr := httptest.NewRecorder()
+		handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
+		require.Equal(t, http.StatusNoContent, rr.Code)
+	})
+
+	t.Run("returns_wrapped_handler", func(t *testing.T) {
+		called := false
+		baseHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(http.StatusOK)
+		})
+
+		f := GetHTTPHandlerFunc(func(base http.Handler) (http.Handler, error) {
+			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				called = true
+				base.ServeHTTP(w, r)
+			}), nil
+		})
+
+		handler, err := f.GetHTTPHandler(baseHandler)
+		require.NoError(t, err)
+		require.NotNil(t, handler)
+
+		rr := httptest.NewRecorder()
+		handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
+		require.True(t, called)
+		require.Equal(t, http.StatusOK, rr.Code)
+	})
+
+	t.Run("returns_error", func(t *testing.T) {
+		expectedErr := errors.New("test error")
+		f := GetHTTPHandlerFunc(func(http.Handler) (http.Handler, error) {
+			return nil, expectedErr
+		})
+
+		baseHandler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
+		handler, err := f.GetHTTPHandler(baseHandler)
+		require.Equal(t, expectedErr, err)
+		require.Nil(t, handler)
+	})
+}
+
+func TestGetGRPCServerOptionsFunc(t *testing.T) {
+	t.Run("nil_function", func(t *testing.T) {
+		var f GetGRPCServerOptionsFunc
+		opts, err := f.GetGRPCServerOptions()
+		require.NoError(t, err)
+		require.Nil(t, opts)
+	})
+
+	t.Run("returns_server_options", func(t *testing.T) {
+		var interceptor grpc.UnaryServerInterceptor = func(
+			context.Context,
+			any,
+			*grpc.UnaryServerInfo,
+			grpc.UnaryHandler,
+		) (resp any, err error) {
+			return nil, nil
+		}
+		expectedOpts := []grpc.ServerOption{grpc.UnaryInterceptor(interceptor)}
+
+		f := GetGRPCServerOptionsFunc(func() ([]grpc.ServerOption, error) {
+			return expectedOpts, nil
+		})
+
+		opts, err := f.GetGRPCServerOptions()
+		require.NoError(t, err)
+		require.Equal(t, expectedOpts, opts)
+	})
+
+	t.Run("returns_error", func(t *testing.T) {
+		expectedErr := errors.New("test error")
+		f := GetGRPCServerOptionsFunc(func() ([]grpc.ServerOption, error) {
+			return nil, expectedErr
+		})
+
+		opts, err := f.GetGRPCServerOptions()
+		require.Equal(t, expectedErr, err)
+		require.Nil(t, opts)
+	})
+}