snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
/*
*
* Copyright 2017 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"sync"
"time"
)
const (
// bdpLimit is the maximum value the flow control windows will be increased
// to. TCP typically limits this to 4MB, but some systems go up to 16MB.
// Since this is only a limit, it is safe to make it optimistic.
bdpLimit = (1 << 20) * 16
// alpha is a constant factor used to keep a moving average
// of RTTs.
alpha = 0.9
// If the current bdp sample is greater than or equal to
// our beta * our estimated bdp and the current bandwidth
// sample is the maximum bandwidth observed so far, we
// increase our bbp estimate by a factor of gamma.
beta = 0.66
// To put our bdp to be smaller than or equal to twice the real BDP,
// we should multiply our current sample with 4/3, however to round things out
// we use 2 as the multiplication factor.
gamma = 2
)
// Adding arbitrary data to ping so that its ack can be identified.
// Easter-egg: what does the ping message say?
var bdpPing = &ping{data: [8]byte{2, 4, 16, 16, 9, 14, 7, 7}}
type bdpEstimator struct {
// sentAt is the time when the ping was sent.
sentAt time.Time
mu sync.Mutex
// bdp is the current bdp estimate.
bdp uint32
// sample is the number of bytes received in one measurement cycle.
sample uint32
// bwMax is the maximum bandwidth noted so far (bytes/sec).
bwMax float64
// bool to keep track of the beginning of a new measurement cycle.
isSent bool
// Callback to update the window sizes.
updateFlowControl func(n uint32)
// sampleCount is the number of samples taken so far.
sampleCount uint64
// round trip time (seconds)
rtt float64
}
// timesnap registers the time bdp ping was sent out so that
// network rtt can be calculated when its ack is received.
// It is called (by controller) when the bdpPing is
// being written on the wire.
func (b *bdpEstimator) timesnap(d [8]byte) {
if bdpPing.data != d {
return
}
b.sentAt = time.Now()
}
// add adds bytes to the current sample for calculating bdp.
// It returns true only if a ping must be sent. This can be used
// by the caller (handleData) to make decision about batching
// a window update with it.
func (b *bdpEstimator) add(n uint32) bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.bdp == bdpLimit {
return false
}
if !b.isSent {
b.isSent = true
b.sample = n
b.sentAt = time.Time{}
b.sampleCount++
return true
}
b.sample += n
return false
}
// calculate is called when an ack for a bdp ping is received.
// Here we calculate the current bdp and bandwidth sample and
// decide if the flow control windows should go up.
func (b *bdpEstimator) calculate(d [8]byte) {
// Check if the ping acked for was the bdp ping.
if bdpPing.data != d {
return
}
b.mu.Lock()
rttSample := time.Since(b.sentAt).Seconds()
if b.sampleCount < 10 {
// Bootstrap rtt with an average of first 10 rtt samples.
b.rtt += (rttSample - b.rtt) / float64(b.sampleCount)
} else {
// Heed to the recent past more.
b.rtt += (rttSample - b.rtt) * float64(alpha)
}
b.isSent = false
// The number of bytes accumulated so far in the sample is smaller
// than or equal to 1.5 times the real BDP on a saturated connection.
bwCurrent := float64(b.sample) / (b.rtt * float64(1.5))
if bwCurrent > b.bwMax {
b.bwMax = bwCurrent
}
// If the current sample (which is smaller than or equal to the 1.5 times the real BDP) is
// greater than or equal to 2/3rd our perceived bdp AND this is the maximum bandwidth seen so far, we
// should update our perception of the network BDP.
if float64(b.sample) >= beta*float64(b.bdp) && bwCurrent == b.bwMax && b.bdp != bdpLimit {
sampleFloat := float64(b.sample)
b.bdp = uint32(gamma * sampleFloat)
if b.bdp > bdpLimit {
b.bdp = bdpLimit
}
bdp := b.bdp
b.mu.Unlock()
b.updateFlowControl(bdp)
return
}
b.mu.Unlock()
}

View File

@@ -0,0 +1,189 @@
/*
*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"fmt"
"sync/atomic"
"golang.org/x/net/http2"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
)
// nonGRPCDataMaxLen is the maximum length of nonGRPCDataBuf.
//
// NOTE: If changed this value, you MUST update the corresponding test in:
// - /test/end2end_test.go:TestHTTPServerSendsNonGRPCHeaderSurfaceFurtherData
const nonGRPCDataMaxLen = 1024
// ClientStream implements streaming functionality for a gRPC client.
type ClientStream struct {
Stream // Embed for common stream functionality.
ct *http2Client
done chan struct{} // closed at the end of stream to unblock writers.
headerChan chan struct{} // closed to indicate the end of header metadata.
header metadata.MD // the received header metadata
status *status.Status // the status error received from the server
// Non-pointer fields are at the end to optimize GC allocations.
// headerValid indicates whether a valid header was received. Only
// meaningful after headerChan is closed (always call waitOnHeader() before
// reading its value).
headerValid bool
nonGRPCStatus *status.Status // the initial status from the non-gRPC response header, finalized with collected data before closing.
nonGRPCDataBuf []byte // stores the data of a non-gRPC response.
noHeaders bool // set if the client never received headers (set only after the stream is done).
headerChanClosed uint32 // set when headerChan is closed. Used to avoid closing headerChan multiple times.
bytesReceived atomic.Bool // indicates whether any bytes have been received on this stream
unprocessed atomic.Bool // set if the server sends a refused stream or GOAWAY including this stream
statsHandler stats.Handler // nil for internal streams (e.g., health check, ORCA) where telemetry is not supported.
}
func (s *ClientStream) startNonGRPCDataCollection(st *status.Status) {
s.nonGRPCStatus = st
s.nonGRPCDataBuf = make([]byte, 0, nonGRPCDataMaxLen)
}
// finalizeNonGRPCStatus builds the terminal status by appending the collected
// response body to the original non-gRPC status message.
func (s *ClientStream) finalizeNonGRPCStatus() *status.Status {
msg := fmt.Sprintf("%s\ndata: %q", s.nonGRPCStatus.Message(), s.nonGRPCDataBuf)
return status.New(s.nonGRPCStatus.Code(), msg)
}
// handleNonGRPCData collects non-gRPC body from the given data frame.
// It returns non-nil value when the stream should be closed with it.
func (s *ClientStream) handleNonGRPCData(f *parsedDataFrame) *status.Status {
n := min(f.data.Len(), nonGRPCDataMaxLen-len(s.nonGRPCDataBuf))
s.nonGRPCDataBuf = append(s.nonGRPCDataBuf, f.data.ReadOnlyData()[0:n]...)
if len(s.nonGRPCDataBuf) >= nonGRPCDataMaxLen || f.StreamEnded() {
return s.finalizeNonGRPCStatus()
}
return nil
}
// Read reads an n byte message from the input stream.
func (s *ClientStream) Read(n int) (mem.BufferSlice, error) {
b, err := s.Stream.read(n)
if err == nil {
s.ct.incrMsgRecv()
}
return b, err
}
// Close closes the stream and propagates err to any readers.
func (s *ClientStream) Close(err error) {
var (
rst bool
rstCode http2.ErrCode
)
if err != nil {
rst = true
rstCode = http2.ErrCodeCancel
}
s.ct.closeStream(s, err, rst, rstCode, status.Convert(err), nil, false)
}
// Write writes the hdr and data bytes to the output stream.
func (s *ClientStream) Write(hdr []byte, data mem.BufferSlice, opts *WriteOptions) error {
return s.ct.write(s, hdr, data, opts)
}
// BytesReceived indicates whether any bytes have been received on this stream.
func (s *ClientStream) BytesReceived() bool {
return s.bytesReceived.Load()
}
// Unprocessed indicates whether the server did not process this stream --
// i.e. it sent a refused stream or GOAWAY including this stream ID.
func (s *ClientStream) Unprocessed() bool {
return s.unprocessed.Load()
}
func (s *ClientStream) waitOnHeader() {
select {
case <-s.ctx.Done():
// Close the stream to prevent headers/trailers from changing after
// this function returns.
s.Close(ContextErr(s.ctx.Err()))
// headerChan could possibly not be closed yet if closeStream raced
// with operateHeaders; wait until it is closed explicitly here.
<-s.headerChan
case <-s.headerChan:
}
}
// RecvCompress returns the compression algorithm applied to the inbound
// message. It is empty string if there is no compression applied.
func (s *ClientStream) RecvCompress() string {
s.waitOnHeader()
return s.recvCompress
}
// Done returns a channel which is closed when it receives the final status
// from the server.
func (s *ClientStream) Done() <-chan struct{} {
return s.done
}
// Header returns the header metadata of the stream. Acquires the key-value
// pairs of header metadata once it is available. It blocks until i) the
// metadata is ready or ii) there is no header metadata or iii) the stream is
// canceled/expired.
func (s *ClientStream) Header() (metadata.MD, error) {
s.waitOnHeader()
if !s.headerValid || s.noHeaders {
return nil, s.status.Err()
}
return s.header.Copy(), nil
}
// TrailersOnly blocks until a header or trailers-only frame is received and
// then returns true if the stream was trailers-only. If the stream ends
// before headers are received, returns true, nil.
func (s *ClientStream) TrailersOnly() bool {
s.waitOnHeader()
return s.noHeaders
}
// Status returns the status received from the server.
// Status can be read safely only after the stream has ended,
// that is, after Done() is closed.
func (s *ClientStream) Status() *status.Status {
return s.status
}
func (s *ClientStream) requestRead(n int) {
s.ct.adjustWindow(s, uint32(n))
}
func (s *ClientStream) updateWindow(n int) {
s.ct.updateWindow(s, uint32(n))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"testing"
"time"
)
func (s) TestControlBuffer_Throttle(t *testing.T) {
done := make(chan struct{})
defer close(done)
cb := newControlBuffer(done)
// Fill the control buffer up to the limit with throttled items.
for i := 0; i < maxQueuedControlBufferItems; i++ {
cb.put(&ping{})
}
// The next call to throttle should block.
throttleDone := make(chan struct{})
go func() {
cb.throttle()
close(throttleDone)
}()
select {
case <-throttleDone:
t.Fatal("throttle() did not block when the buffer was full")
case <-time.After(defaultTestShortTimeout):
}
// Consume one item from the control buffer.
if _, err := cb.get(true); err != nil {
t.Fatalf("cb.get(true) failed: %v", err)
}
// Now throttle() should unblock.
select {
case <-throttleDone:
case <-time.After(time.Second):
t.Fatal("throttle() did not unblock after an item was consumed")
}
}
func (s) TestControlBuffer_NoThrottleForNonThrottledItems(t *testing.T) {
done := make(chan struct{})
defer close(done)
cb := newControlBuffer(done)
// Fill the control buffer with many more than limit number of non-throttled
// items.
for i := 0; i < maxQueuedControlBufferItems+10; i++ {
cb.put(&dataFrame{})
}
// throttle() should not block.
throttled := make(chan struct{})
go func() {
cb.throttle()
close(throttled)
}()
select {
case <-throttled:
case <-time.After(defaultTestShortTimeout):
t.Fatal("throttle() blocked for non-throttled items")
}
}

View File

@@ -0,0 +1,56 @@
/*
*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"math"
"time"
)
const (
// The default value of flow control window size in HTTP2 spec.
defaultWindowSize = 65535
// The initial window size for flow control.
initialWindowSize = defaultWindowSize // for an RPC
infinity = time.Duration(math.MaxInt64)
defaultClientKeepaliveTime = infinity
defaultClientKeepaliveTimeout = 20 * time.Second
defaultMaxStreamsClient = 100
defaultMaxConnectionIdle = infinity
defaultMaxConnectionAge = infinity
defaultMaxConnectionAgeGrace = infinity
defaultServerKeepaliveTime = 2 * time.Hour
defaultServerKeepaliveTimeout = 20 * time.Second
defaultKeepalivePolicyMinTime = 5 * time.Minute
// max window limit set by HTTP2 Specs.
maxWindowSize = math.MaxInt32
// defaultWriteQuota is the default value for number of data
// bytes that each stream can schedule before some of it being
// flushed out.
defaultWriteQuota = 64 * 1024
defaultClientMaxHeaderListSize = uint32(16 << 20)
defaultServerMaxHeaderListSize = uint32(16 << 20)
upcomingDefaultHeaderListSize = uint32(8 << 10)
)
// MaxStreamID is the upper bound for the stream ID before the current
// transport gracefully closes and new transport is created for subsequent RPCs.
// This is set to 75% of 2^31-1. Streams are identified with an unsigned 31-bit
// integer. It's exported so that tests can override it.
var MaxStreamID = uint32(math.MaxInt32 * 3 / 4)

View File

@@ -0,0 +1,211 @@
/*
*
* Copyright 2014 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"fmt"
"math"
"sync"
"sync/atomic"
)
// writeQuota is a soft limit on the amount of data a stream can
// schedule before some of it is written out.
type writeQuota struct {
_ noCopy
// get waits on read from when quota goes less than or equal to zero.
// replenish writes on it when quota goes positive again.
ch chan struct{}
// done is triggered in error case.
done <-chan struct{}
// replenish is called by loopyWriter to give quota back to.
// It is implemented as a field so that it can be updated
// by tests.
replenish func(n int)
quota int32
}
// init allows a writeQuota to be initialized in-place, which is useful for
// resetting a buffer or for avoiding a heap allocation when the buffer is
// embedded in another struct.
func (w *writeQuota) init(sz int32, done <-chan struct{}) {
w.quota = sz
w.ch = make(chan struct{}, 1)
w.done = done
w.replenish = w.realReplenish
}
func (w *writeQuota) get(sz int32) error {
for {
if atomic.LoadInt32(&w.quota) > 0 {
atomic.AddInt32(&w.quota, -sz)
return nil
}
select {
case <-w.ch:
continue
case <-w.done:
return errStreamDone
}
}
}
func (w *writeQuota) realReplenish(n int) {
sz := int32(n)
newQuota := atomic.AddInt32(&w.quota, sz)
previousQuota := newQuota - sz
if previousQuota <= 0 && newQuota > 0 {
select {
case w.ch <- struct{}{}:
default:
}
}
}
type trInFlow struct {
limit uint32
unacked uint32
effectiveWindowSize uint32
}
func (f *trInFlow) newLimit(n uint32) uint32 {
d := n - f.limit
f.limit = n
f.updateEffectiveWindowSize()
return d
}
func (f *trInFlow) onData(n uint32) uint32 {
f.unacked += n
if f.unacked < f.limit/4 {
f.updateEffectiveWindowSize()
return 0
}
return f.reset()
}
func (f *trInFlow) reset() uint32 {
w := f.unacked
f.unacked = 0
f.updateEffectiveWindowSize()
return w
}
func (f *trInFlow) updateEffectiveWindowSize() {
atomic.StoreUint32(&f.effectiveWindowSize, f.limit-f.unacked)
}
func (f *trInFlow) getSize() uint32 {
return atomic.LoadUint32(&f.effectiveWindowSize)
}
// inFlow deals with inbound flow control
type inFlow struct {
mu sync.Mutex
// The inbound flow control limit for pending data.
limit uint32
// pendingData is the overall data which have been received but not been
// consumed by applications.
pendingData uint32
// The amount of data the application has consumed but grpc has not sent
// window update for them. Used to reduce window update frequency.
pendingUpdate uint32
// delta is the extra window update given by receiver when an application
// is reading data bigger in size than the inFlow limit.
delta uint32
}
// newLimit updates the inflow window to a new value n.
// It assumes that n is always greater than the old limit.
func (f *inFlow) newLimit(n uint32) {
f.mu.Lock()
f.limit = n
f.mu.Unlock()
}
func (f *inFlow) maybeAdjust(n uint32) uint32 {
if n > uint32(math.MaxInt32) {
n = uint32(math.MaxInt32)
}
f.mu.Lock()
defer f.mu.Unlock()
// estSenderQuota is the receiver's view of the maximum number of bytes the sender
// can send without a window update.
estSenderQuota := int32(f.limit - (f.pendingData + f.pendingUpdate))
// estUntransmittedData is the maximum number of bytes the sends might not have put
// on the wire yet. A value of 0 or less means that we have already received all or
// more bytes than the application is requesting to read.
estUntransmittedData := int32(n - f.pendingData) // Casting into int32 since it could be negative.
// This implies that unless we send a window update, the sender won't be able to send all the bytes
// for this message. Therefore we must send an update over the limit since there's an active read
// request from the application.
if estUntransmittedData > estSenderQuota {
// Sender's window shouldn't go more than 2^31 - 1 as specified in the HTTP spec.
if f.limit+n > maxWindowSize {
f.delta = maxWindowSize - f.limit
} else {
// Send a window update for the whole message and not just the difference between
// estUntransmittedData and estSenderQuota. This will be helpful in case the message
// is padded; We will fallback on the current available window(at least a 1/4th of the limit).
f.delta = n
}
return f.delta
}
return 0
}
// onData is invoked when some data frame is received. It updates pendingData.
func (f *inFlow) onData(n uint32) error {
f.mu.Lock()
defer f.mu.Unlock()
f.pendingData += n
if f.pendingData+f.pendingUpdate > f.limit+f.delta {
limit := f.limit
rcvd := f.pendingData + f.pendingUpdate
return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", rcvd, limit)
}
return nil
}
// onRead is invoked when the application reads the data. It returns the window size
// to be sent to the peer.
func (f *inFlow) onRead(n uint32) uint32 {
f.mu.Lock()
defer f.mu.Unlock()
if f.pendingData == 0 {
return 0
}
f.pendingData -= n
if n > f.delta {
n -= f.delta
f.delta = 0
} else {
f.delta -= n
n = 0
}
f.pendingUpdate += n
if f.pendingUpdate >= f.limit/4 {
wu := f.pendingUpdate
f.pendingUpdate = 0
return wu
}
return 0
}

View File

@@ -0,0 +1,506 @@
/*
*
* Copyright 2016 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// This file is the implementation of a gRPC server using HTTP/2 which
// uses the standard Go http2 Server implementation (via the
// http.Handler interface), rather than speaking low-level HTTP/2
// frames itself. It is the implementation of *grpc.Server.ServeHTTP.
package transport
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"golang.org/x/net/http2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcutil"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// NewServerHandlerTransport returns a ServerTransport handling gRPC from
// inside an http.Handler, or writes an HTTP error to w and returns an error.
// It requires that the http Server supports HTTP/2.
func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats stats.Handler, bufferPool mem.BufferPool) (ServerTransport, error) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
msg := fmt.Sprintf("invalid gRPC request method %q", r.Method)
http.Error(w, msg, http.StatusMethodNotAllowed)
return nil, errors.New(msg)
}
contentType := r.Header.Get("Content-Type")
// TODO: do we assume contentType is lowercase? we did before
contentSubtype, validContentType := grpcutil.ContentSubtype(contentType)
if !validContentType {
msg := fmt.Sprintf("invalid gRPC request content-type %q", contentType)
http.Error(w, msg, http.StatusUnsupportedMediaType)
return nil, errors.New(msg)
}
if r.ProtoMajor != 2 {
msg := "gRPC requires HTTP/2"
http.Error(w, msg, http.StatusHTTPVersionNotSupported)
return nil, errors.New(msg)
}
if _, ok := w.(http.Flusher); !ok {
msg := "gRPC requires a ResponseWriter supporting http.Flusher"
http.Error(w, msg, http.StatusInternalServerError)
return nil, errors.New(msg)
}
var localAddr net.Addr
if la := r.Context().Value(http.LocalAddrContextKey); la != nil {
localAddr, _ = la.(net.Addr)
}
var authInfo credentials.AuthInfo
if r.TLS != nil {
authInfo = credentials.TLSInfo{State: *r.TLS, CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}
}
p := peer.Peer{
Addr: strAddr(r.RemoteAddr),
LocalAddr: localAddr,
AuthInfo: authInfo,
}
st := &serverHandlerTransport{
rw: w,
req: r,
closedCh: make(chan struct{}),
writes: make(chan func()),
peer: p,
contentType: contentType,
contentSubtype: contentSubtype,
stats: stats,
bufferPool: bufferPool,
}
st.logger = prefixLoggerForServerHandlerTransport(st)
if v := r.Header.Get("grpc-timeout"); v != "" {
to, err := decodeTimeout(v)
if err != nil {
msg := fmt.Sprintf("malformed grpc-timeout: %v", err)
http.Error(w, msg, http.StatusBadRequest)
return nil, status.Error(codes.Internal, msg)
}
st.timeoutSet = true
st.timeout = to
}
metakv := []string{"content-type", contentType}
if r.Host != "" {
metakv = append(metakv, ":authority", r.Host)
}
for k, vv := range r.Header {
k = strings.ToLower(k)
if isReservedHeader(k) && !isWhitelistedHeader(k) {
continue
}
for _, v := range vv {
v, err := decodeMetadataHeader(k, v)
if err != nil {
msg := fmt.Sprintf("malformed binary metadata %q in header %q: %v", v, k, err)
http.Error(w, msg, http.StatusBadRequest)
return nil, status.Error(codes.Internal, msg)
}
metakv = append(metakv, k, v)
}
}
st.headerMD = metadata.Pairs(metakv...)
return st, nil
}
// serverHandlerTransport is an implementation of ServerTransport
// which replies to exactly one gRPC request (exactly one HTTP request),
// using the net/http.Handler interface. This http.Handler is guaranteed
// at this point to be speaking over HTTP/2, so it's able to speak valid
// gRPC.
type serverHandlerTransport struct {
rw http.ResponseWriter
req *http.Request
timeoutSet bool
timeout time.Duration
headerMD metadata.MD
peer peer.Peer
closeOnce sync.Once
closedCh chan struct{} // closed on Close
// writes is a channel of code to run serialized in the
// ServeHTTP (HandleStreams) goroutine. The channel is closed
// when WriteStatus is called.
writes chan func()
// block concurrent WriteStatus calls
// e.g. grpc/(*serverStream).SendMsg/RecvMsg
writeStatusMu sync.Mutex
// we just mirror the request content-type
contentType string
// we store both contentType and contentSubtype so we don't keep recreating them
// TODO make sure this is consistent across handler_server and http2_server
contentSubtype string
stats stats.Handler
logger *grpclog.PrefixLogger
bufferPool mem.BufferPool
}
func (ht *serverHandlerTransport) Close(err error) {
ht.closeOnce.Do(func() {
if ht.logger.V(logLevel) {
ht.logger.Infof("Closing: %v", err)
}
close(ht.closedCh)
})
}
func (ht *serverHandlerTransport) Peer() *peer.Peer {
return &peer.Peer{
Addr: ht.peer.Addr,
LocalAddr: ht.peer.LocalAddr,
AuthInfo: ht.peer.AuthInfo,
}
}
// strAddr is a net.Addr backed by either a TCP "ip:port" string, or
// the empty string if unknown.
type strAddr string
func (a strAddr) Network() string {
if a != "" {
// Per the documentation on net/http.Request.RemoteAddr, if this is
// set, it's set to the IP:port of the peer (hence, TCP):
// https://golang.org/pkg/net/http/#Request
//
// If we want to support Unix sockets later, we can
// add our own grpc-specific convention within the
// grpc codebase to set RemoteAddr to a different
// format, or probably better: we can attach it to the
// context and use that from serverHandlerTransport.RemoteAddr.
return "tcp"
}
return ""
}
func (a strAddr) String() string { return string(a) }
// do runs fn in the ServeHTTP goroutine.
func (ht *serverHandlerTransport) do(fn func()) error {
select {
case <-ht.closedCh:
return ErrConnClosing
case ht.writes <- fn:
return nil
}
}
func (ht *serverHandlerTransport) writeStatus(s *ServerStream, st *status.Status) error {
ht.writeStatusMu.Lock()
defer ht.writeStatusMu.Unlock()
headersWritten := s.updateHeaderSent()
err := ht.do(func() {
if !headersWritten {
ht.writePendingHeaders(s)
}
// And flush, in case no header or body has been sent yet.
// This forces a separation of headers and trailers if this is the
// first call (for example, in end2end tests's TestNoService).
ht.rw.(http.Flusher).Flush()
h := ht.rw.Header()
h.Set("Grpc-Status", fmt.Sprintf("%d", st.Code()))
if m := st.Message(); m != "" {
h.Set("Grpc-Message", encodeGrpcMessage(m))
}
s.hdrMu.Lock()
defer s.hdrMu.Unlock()
if p := st.Proto(); p != nil && len(p.Details) > 0 {
delete(s.trailer, grpcStatusDetailsBinHeader)
stBytes, err := proto.Marshal(p)
if err != nil {
// TODO: return error instead, when callers are able to handle it.
panic(err)
}
h.Set(grpcStatusDetailsBinHeader, encodeBinHeader(stBytes))
}
if len(s.trailer) > 0 {
for k, vv := range s.trailer {
// Clients don't tolerate reading restricted headers after some non restricted ones were sent.
if isReservedHeader(k) {
continue
}
for _, v := range vv {
// http2 ResponseWriter mechanism to send undeclared Trailers after
// the headers have possibly been written.
h.Add(http2.TrailerPrefix+k, encodeMetadataHeader(k, v))
}
}
}
})
if err == nil && ht.stats != nil { // transport has not been closed
// Note: The trailer fields are compressed with hpack after this call returns.
// No WireLength field is set here.
s.hdrMu.Lock()
ht.stats.HandleRPC(s.Context(), &stats.OutTrailer{
Trailer: s.trailer.Copy(),
})
s.hdrMu.Unlock()
}
ht.Close(errors.New("finished writing status"))
return err
}
// writePendingHeaders sets common and custom headers on the first
// write call (Write, WriteHeader, or WriteStatus)
func (ht *serverHandlerTransport) writePendingHeaders(s *ServerStream) {
ht.writeCommonHeaders(s)
ht.writeCustomHeaders(s)
}
// writeCommonHeaders sets common headers on the first write
// call (Write, WriteHeader, or WriteStatus).
func (ht *serverHandlerTransport) writeCommonHeaders(s *ServerStream) {
h := ht.rw.Header()
h["Date"] = nil // suppress Date to make tests happy; TODO: restore
h.Set("Content-Type", ht.contentType)
// Predeclare trailers we'll set later in WriteStatus (after the body).
// This is a SHOULD in the HTTP RFC, and the way you add (known)
// Trailers per the net/http.ResponseWriter contract.
// See https://golang.org/pkg/net/http/#ResponseWriter
// and https://golang.org/pkg/net/http/#example_ResponseWriter_trailers
h.Add("Trailer", "Grpc-Status")
h.Add("Trailer", "Grpc-Message")
h.Add("Trailer", "Grpc-Status-Details-Bin")
if s.sendCompress != "" {
h.Set("Grpc-Encoding", s.sendCompress)
}
}
// writeCustomHeaders sets custom headers set on the stream via SetHeader
// on the first write call (Write, WriteHeader, or WriteStatus)
func (ht *serverHandlerTransport) writeCustomHeaders(s *ServerStream) {
h := ht.rw.Header()
s.hdrMu.Lock()
for k, vv := range s.header {
if isReservedHeader(k) {
continue
}
for _, v := range vv {
h.Add(k, encodeMetadataHeader(k, v))
}
}
s.hdrMu.Unlock()
}
func (ht *serverHandlerTransport) write(s *ServerStream, hdr []byte, data mem.BufferSlice, _ *WriteOptions) error {
// Always take a reference because otherwise there is no guarantee the data will
// be available after this function returns. This is what callers to Write
// expect.
data.Ref()
headersWritten := s.updateHeaderSent()
err := ht.do(func() {
defer data.Free()
if !headersWritten {
ht.writePendingHeaders(s)
}
ht.rw.Write(hdr)
for _, b := range data {
_, _ = ht.rw.Write(b.ReadOnlyData())
}
ht.rw.(http.Flusher).Flush()
})
if err != nil {
data.Free()
return err
}
return nil
}
func (ht *serverHandlerTransport) writeHeader(s *ServerStream, md metadata.MD) error {
if err := s.SetHeader(md); err != nil {
return err
}
headersWritten := s.updateHeaderSent()
err := ht.do(func() {
if !headersWritten {
ht.writePendingHeaders(s)
}
ht.rw.WriteHeader(200)
ht.rw.(http.Flusher).Flush()
})
if err == nil && ht.stats != nil {
// Note: The header fields are compressed with hpack after this call returns.
// No WireLength field is set here.
ht.stats.HandleRPC(s.Context(), &stats.OutHeader{
Header: md.Copy(),
Compression: s.sendCompress,
})
}
return err
}
func (ht *serverHandlerTransport) adjustWindow(*ServerStream, uint32) {
}
func (ht *serverHandlerTransport) updateWindow(*ServerStream, uint32) {
}
func (ht *serverHandlerTransport) HandleStreams(ctx context.Context, startStream func(*ServerStream)) {
// With this transport type there will be exactly 1 stream: this HTTP request.
var cancel context.CancelFunc
if ht.timeoutSet {
ctx, cancel = context.WithTimeout(ctx, ht.timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
// requestOver is closed when the status has been written via WriteStatus.
requestOver := make(chan struct{})
go func() {
select {
case <-requestOver:
case <-ht.closedCh:
case <-ht.req.Context().Done():
}
cancel()
ht.Close(errors.New("request is done processing"))
}()
ctx = metadata.NewIncomingContext(ctx, ht.headerMD)
req := ht.req
s := &ServerStream{
Stream: Stream{
id: 0, // irrelevant
ctx: ctx,
method: req.URL.Path,
recvCompress: req.Header.Get("grpc-encoding"),
contentSubtype: ht.contentSubtype,
},
cancel: cancel,
st: ht,
headerWireLength: 0, // won't have access to header wire length until golang/go#18997.
}
s.Stream.buf.init(ht.bufferPool)
s.readRequester = s
s.trReader = transportReader{
reader: recvBufferReader{ctx: s.ctx, ctxDone: s.ctx.Done(), recv: &s.buf},
windowHandler: s,
}
// readerDone is closed when the Body.Read-ing goroutine exits.
readerDone := make(chan struct{})
go func() {
defer close(readerDone)
for {
buf := ht.bufferPool.Get(http2MaxFrameLen)
n, err := req.Body.Read(*buf)
if n > 0 {
*buf = (*buf)[:n]
s.buf.put(recvMsg{buffer: mem.NewBuffer(buf, ht.bufferPool)})
} else {
ht.bufferPool.Put(buf)
}
if err != nil {
s.buf.put(recvMsg{err: mapRecvMsgError(err)})
return
}
}
}()
// startStream is provided by the *grpc.Server's serveStreams.
// It starts a goroutine serving s and exits immediately.
// The goroutine that is started is the one that then calls
// into ht, calling WriteHeader, Write, WriteStatus, Close, etc.
startStream(s)
ht.runStream()
close(requestOver)
// Wait for reading goroutine to finish.
req.Body.Close()
<-readerDone
}
func (ht *serverHandlerTransport) runStream() {
for {
select {
case fn := <-ht.writes:
fn()
case <-ht.closedCh:
return
}
}
}
func (ht *serverHandlerTransport) incrMsgRecv() {}
func (ht *serverHandlerTransport) Drain(s string) {
ht.Close(errors.New(s))
}
// mapRecvMsgError returns the non-nil err into the appropriate
// error value as expected by callers of *grpc.parser.recvMsg.
// In particular, in can only be:
// - io.EOF
// - io.ErrUnexpectedEOF
// - of type transport.ConnectionError
// - an error from the status package
func mapRecvMsgError(err error) error {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return err
}
if se, ok := err.(http2.StreamError); ok {
if code, ok := http2ErrConvTab[se.Code]; ok {
return status.Error(code, se.Error())
}
}
if strings.Contains(err.Error(), "body closed by handler") {
return status.Error(codes.Canceled, err.Error())
}
return connectionErrorf(true, err, "%s", err.Error())
}

View File

@@ -0,0 +1,614 @@
/*
*
* Copyright 2016 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"sync"
"testing"
"time"
epb "google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"
"google.golang.org/protobuf/types/known/durationpb"
)
func (s) TestHandlerTransport_NewServerHandlerTransport(t *testing.T) {
type testCase struct {
name string
req *http.Request
wantErr string
wantErrCode int
modrw func(http.ResponseWriter) http.ResponseWriter
check func(*serverHandlerTransport, *testCase) error
}
tests := []testCase{
{
name: "bad method",
req: &http.Request{
ProtoMajor: 2,
Method: "GET",
Header: http.Header{},
},
wantErr: `invalid gRPC request method "GET"`,
wantErrCode: http.StatusMethodNotAllowed,
},
{
name: "bad content type",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": {"application/foo"},
},
},
wantErr: `invalid gRPC request content-type "application/foo"`,
wantErrCode: http.StatusUnsupportedMediaType,
},
{
name: "http/1.1",
req: &http.Request{
ProtoMajor: 1,
ProtoMinor: 1,
Method: "POST",
Header: http.Header{"Content-Type": []string{"application/grpc"}},
},
wantErr: "gRPC requires HTTP/2",
wantErrCode: http.StatusHTTPVersionNotSupported,
},
{
name: "not flusher",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": {"application/grpc"},
},
},
modrw: func(w http.ResponseWriter) http.ResponseWriter {
// Return w without its Flush method
type onlyCloseNotifier interface {
http.ResponseWriter
}
return struct{ onlyCloseNotifier }{w.(onlyCloseNotifier)}
},
wantErr: "gRPC requires a ResponseWriter supporting http.Flusher",
wantErrCode: http.StatusInternalServerError,
},
{
name: "valid",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": {"application/grpc"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
},
check: func(t *serverHandlerTransport, tt *testCase) error {
if t.req != tt.req {
return fmt.Errorf("t.req = %p; want %p", t.req, tt.req)
}
if t.rw == nil {
return errors.New("t.rw = nil; want non-nil")
}
return nil
},
},
{
name: "with timeout",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": []string{"application/grpc"},
"Grpc-Timeout": {"200m"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
},
check: func(t *serverHandlerTransport, _ *testCase) error {
if !t.timeoutSet {
return errors.New("timeout not set")
}
if want := 200 * time.Millisecond; t.timeout != want {
return fmt.Errorf("timeout = %v; want %v", t.timeout, want)
}
return nil
},
},
{
name: "with bad timeout",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": []string{"application/grpc"},
"Grpc-Timeout": {"tomorrow"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
},
wantErr: `rpc error: code = Internal desc = malformed grpc-timeout: transport: timeout unit is not recognized: "tomorrow"`,
wantErrCode: http.StatusBadRequest,
},
{
name: "with metadata",
req: &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": []string{"application/grpc"},
"meta-foo": {"foo-val"},
"meta-bar": {"bar-val1", "bar-val2"},
"user-agent": {"x/y a/b"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
},
check: func(ht *serverHandlerTransport, _ *testCase) error {
want := metadata.MD{
"meta-bar": {"bar-val1", "bar-val2"},
"user-agent": {"x/y a/b"},
"meta-foo": {"foo-val"},
"content-type": {"application/grpc"},
}
if !reflect.DeepEqual(ht.headerMD, want) {
return fmt.Errorf("metadata = %#v; want %#v", ht.headerMD, want)
}
return nil
},
},
}
for _, tt := range tests {
rrec := httptest.NewRecorder()
rw := http.ResponseWriter(testHandlerResponseWriter{
ResponseRecorder: rrec,
})
if tt.modrw != nil {
rw = tt.modrw(rw)
}
got, gotErr := NewServerHandlerTransport(rw, tt.req, nil, mem.DefaultBufferPool())
if (gotErr != nil) != (tt.wantErr != "") || (gotErr != nil && gotErr.Error() != tt.wantErr) {
t.Errorf("%s: error = %q; want %q", tt.name, gotErr.Error(), tt.wantErr)
continue
}
if tt.wantErrCode == 0 {
tt.wantErrCode = http.StatusOK
}
if rrec.Code != tt.wantErrCode {
t.Errorf("%s: code = %d; want %d", tt.name, rrec.Code, tt.wantErrCode)
continue
}
if gotErr != nil {
continue
}
if tt.check != nil {
if err := tt.check(got.(*serverHandlerTransport), &tt); err != nil {
t.Errorf("%s: %v", tt.name, err)
}
}
}
}
type testHandlerResponseWriter struct {
*httptest.ResponseRecorder
}
func (w testHandlerResponseWriter) Flush() {}
func newTestHandlerResponseWriter() http.ResponseWriter {
return testHandlerResponseWriter{
ResponseRecorder: httptest.NewRecorder(),
}
}
type handleStreamTest struct {
t *testing.T
bodyw *io.PipeWriter
rw testHandlerResponseWriter
ht *serverHandlerTransport
}
type mockStatsHandler struct {
rpcStatsCh chan stats.RPCStats
}
func (h *mockStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
return ctx
}
func (h *mockStatsHandler) HandleRPC(_ context.Context, s stats.RPCStats) {
h.rpcStatsCh <- s
}
func (h *mockStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return ctx
}
func (h *mockStatsHandler) HandleConn(context.Context, stats.ConnStats) {
}
func newHandleStreamTest(t *testing.T, statsHandler stats.Handler) *handleStreamTest {
bodyr, bodyw := io.Pipe()
req := &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": {"application/grpc"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
Body: bodyr,
}
rw := newTestHandlerResponseWriter().(testHandlerResponseWriter)
ht, err := NewServerHandlerTransport(rw, req, statsHandler, mem.DefaultBufferPool())
if err != nil {
t.Fatal(err)
}
return &handleStreamTest{
t: t,
bodyw: bodyw,
ht: ht.(*serverHandlerTransport),
rw: rw,
}
}
func (s) TestHandlerTransport_HandleStreams(t *testing.T) {
st := newHandleStreamTest(t, nil)
handleStream := func(s *ServerStream) {
if want := "/service/foo.bar"; s.method != want {
t.Errorf("stream method = %q; want %q", s.method, want)
}
if err := s.SetHeader(metadata.Pairs("custom-header", "Custom header value")); err != nil {
t.Error(err)
}
if err := s.SetTrailer(metadata.Pairs("custom-trailer", "Custom trailer value")); err != nil {
t.Error(err)
}
if err := s.SetSendCompress("gzip"); err != nil {
t.Error(err)
}
md := metadata.Pairs("custom-header", "Another custom header value")
if err := s.SendHeader(md); err != nil {
t.Error(err)
}
delete(md, "custom-header")
if err := s.SetHeader(metadata.Pairs("too-late", "Header value that should be ignored")); err == nil {
t.Error("expected SetHeader call after SendHeader to fail")
}
if err := s.SendHeader(metadata.Pairs("too-late", "This header value should be ignored as well")); err == nil {
t.Error("expected second SendHeader call to fail")
}
if err := s.SetSendCompress("snappy"); err == nil {
t.Error("expected second SetSendCompress call to fail")
}
st.bodyw.Close() // no body
s.WriteStatus(status.New(codes.OK, ""))
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
st.ht.HandleStreams(
ctx, func(s *ServerStream) { go handleStream(s) },
)
wantHeader := http.Header{
"Date": nil,
"Content-Type": {"application/grpc"},
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
"Custom-Header": {"Custom header value", "Another custom header value"},
"Grpc-Encoding": {"gzip"},
}
wantTrailer := http.Header{
"Grpc-Status": {"0"},
"Custom-Trailer": {"Custom trailer value"},
}
checkHeaderAndTrailer(t, st.rw, wantHeader, wantTrailer)
}
// Tests that codes.Unimplemented will close the body, per comment in handler_server.go.
func (s) TestHandlerTransport_HandleStreams_Unimplemented(t *testing.T) {
handleStreamCloseBodyTest(t, codes.Unimplemented, "thingy is unimplemented")
}
// Tests that codes.InvalidArgument will close the body, per comment in handler_server.go.
func (s) TestHandlerTransport_HandleStreams_InvalidArgument(t *testing.T) {
handleStreamCloseBodyTest(t, codes.InvalidArgument, "bad arg")
}
func handleStreamCloseBodyTest(t *testing.T, statusCode codes.Code, msg string) {
st := newHandleStreamTest(t, nil)
handleStream := func(s *ServerStream) {
s.WriteStatus(status.New(statusCode, msg))
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
st.ht.HandleStreams(
ctx, func(s *ServerStream) { go handleStream(s) },
)
wantHeader := http.Header{
"Date": nil,
"Content-Type": {"application/grpc"},
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
}
wantTrailer := http.Header{
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
"Grpc-Message": {encodeGrpcMessage(msg)},
}
checkHeaderAndTrailer(t, st.rw, wantHeader, wantTrailer)
}
func (s) TestHandlerTransport_HandleStreams_Timeout(t *testing.T) {
bodyr, bodyw := io.Pipe()
req := &http.Request{
ProtoMajor: 2,
Method: "POST",
Header: http.Header{
"Content-Type": {"application/grpc"},
"Grpc-Timeout": {"200m"},
},
URL: &url.URL{
Path: "/service/foo.bar",
},
Body: bodyr,
}
rw := newTestHandlerResponseWriter().(testHandlerResponseWriter)
ht, err := NewServerHandlerTransport(rw, req, nil, mem.DefaultBufferPool())
if err != nil {
t.Fatal(err)
}
runStream := func(s *ServerStream) {
defer bodyw.Close()
select {
case <-s.ctx.Done():
case <-time.After(5 * time.Second):
t.Errorf("timeout waiting for ctx.Done")
return
}
err := s.ctx.Err()
if err != context.DeadlineExceeded {
t.Errorf("ctx.Err = %v; want %v", err, context.DeadlineExceeded)
return
}
s.WriteStatus(status.New(codes.DeadlineExceeded, "too slow"))
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ht.HandleStreams(
ctx, func(s *ServerStream) { go runStream(s) },
)
wantHeader := http.Header{
"Date": nil,
"Content-Type": {"application/grpc"},
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
}
wantTrailer := http.Header{
"Grpc-Status": {"4"},
"Grpc-Message": {encodeGrpcMessage("too slow")},
}
checkHeaderAndTrailer(t, rw, wantHeader, wantTrailer)
}
// TestHandlerTransport_HandleStreams_MultiWriteStatus ensures that
// concurrent "WriteStatus"s do not panic writing to closed "writes" channel.
func (s) TestHandlerTransport_HandleStreams_MultiWriteStatus(t *testing.T) {
testHandlerTransportHandleStreams(t, func(st *handleStreamTest, s *ServerStream) {
if want := "/service/foo.bar"; s.method != want {
t.Errorf("stream method = %q; want %q", s.method, want)
}
st.bodyw.Close() // no body
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
s.WriteStatus(status.New(codes.OK, ""))
}()
}
wg.Wait()
})
}
// TestHandlerTransport_HandleStreams_WriteStatusWrite ensures that "Write"
// following "WriteStatus" does not panic writing to closed "writes" channel.
func (s) TestHandlerTransport_HandleStreams_WriteStatusWrite(t *testing.T) {
testHandlerTransportHandleStreams(t, func(st *handleStreamTest, s *ServerStream) {
if want := "/service/foo.bar"; s.method != want {
t.Errorf("stream method = %q; want %q", s.method, want)
}
st.bodyw.Close() // no body
s.WriteStatus(status.New(codes.OK, ""))
s.Write([]byte("hdr"), newBufferSlice([]byte("data")), &WriteOptions{})
})
}
func testHandlerTransportHandleStreams(t *testing.T, handleStream func(st *handleStreamTest, s *ServerStream)) {
st := newHandleStreamTest(t, nil)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
t.Cleanup(cancel)
st.ht.HandleStreams(
ctx, func(s *ServerStream) { go handleStream(st, s) },
)
}
func (s) TestHandlerTransport_HandleStreams_ErrDetails(t *testing.T) {
errDetails := []protoadapt.MessageV1{
&epb.RetryInfo{
RetryDelay: &durationpb.Duration{Seconds: 60},
},
&epb.ResourceInfo{
ResourceType: "foo bar",
ResourceName: "service.foo.bar",
Owner: "User",
},
}
statusCode := codes.ResourceExhausted
msg := "you are being throttled"
st, err := status.New(statusCode, msg).WithDetails(errDetails...)
if err != nil {
t.Fatal(err)
}
stBytes, err := proto.Marshal(st.Proto())
if err != nil {
t.Fatal(err)
}
hst := newHandleStreamTest(t, nil)
handleStream := func(s *ServerStream) {
s.WriteStatus(st)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
hst.ht.HandleStreams(
ctx, func(s *ServerStream) { go handleStream(s) },
)
wantHeader := http.Header{
"Date": nil,
"Content-Type": {"application/grpc"},
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
}
wantTrailer := http.Header{
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
"Grpc-Message": {encodeGrpcMessage(msg)},
"Grpc-Status-Details-Bin": {encodeBinHeader(stBytes)},
}
checkHeaderAndTrailer(t, hst.rw, wantHeader, wantTrailer)
}
// Tests the use of stats handlers and ensures there are no data races while
// accessing trailers.
func (s) TestHandlerTransport_HandleStreams_StatsHandlers(t *testing.T) {
errDetails := []protoadapt.MessageV1{
&epb.RetryInfo{
RetryDelay: &durationpb.Duration{Seconds: 60},
},
&epb.ResourceInfo{
ResourceType: "foo bar",
ResourceName: "service.foo.bar",
Owner: "User",
},
}
statusCode := codes.ResourceExhausted
msg := "you are being throttled"
st, err := status.New(statusCode, msg).WithDetails(errDetails...)
if err != nil {
t.Fatal(err)
}
stBytes, err := proto.Marshal(st.Proto())
if err != nil {
t.Fatal(err)
}
// Add mock stats handlers to exercise the stats handler code path.
statsHandler := &mockStatsHandler{
rpcStatsCh: make(chan stats.RPCStats, 2),
}
hst := newHandleStreamTest(t, statsHandler)
handleStream := func(s *ServerStream) {
if err := s.SendHeader(metadata.New(map[string]string{})); err != nil {
t.Error(err)
}
if err := s.SetTrailer(metadata.Pairs("custom-trailer", "Custom trailer value")); err != nil {
t.Error(err)
}
s.WriteStatus(st)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
hst.ht.HandleStreams(
ctx, func(s *ServerStream) { go handleStream(s) },
)
wantHeader := http.Header{
"Date": nil,
"Content-Type": {"application/grpc"},
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
}
wantTrailer := http.Header{
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
"Grpc-Message": {encodeGrpcMessage(msg)},
"Grpc-Status-Details-Bin": {encodeBinHeader(stBytes)},
"Custom-Trailer": []string{"Custom trailer value"},
}
checkHeaderAndTrailer(t, hst.rw, wantHeader, wantTrailer)
wantStatTypes := []stats.RPCStats{&stats.OutHeader{}, &stats.OutTrailer{}}
for _, wantType := range wantStatTypes {
select {
case <-ctx.Done():
t.Fatal("Context timed out waiting for statsHandler.HandleRPC() to be called.")
case s := <-statsHandler.rpcStatsCh:
if reflect.TypeOf(s) != reflect.TypeOf(wantType) {
t.Fatalf("Received RPCStats of type %T, want %T", s, wantType)
}
}
}
}
// checkHeaderAndTrailer checks that the resulting header and trailer matches the expectation.
func checkHeaderAndTrailer(t *testing.T, rw testHandlerResponseWriter, wantHeader, wantTrailer http.Header) {
// For trailer-only responses, the trailer values might be reported as part of the Header. They will however
// be present in Trailer in either case. Hence, normalize the header by removing all trailer values.
actualHeader := rw.Result().Header.Clone()
for _, trailerKey := range actualHeader["Trailer"] {
actualHeader.Del(trailerKey)
}
if !reflect.DeepEqual(actualHeader, wantHeader) {
t.Errorf("Header mismatch.\n got: %#v\n want: %#v", actualHeader, wantHeader)
}
if actualTrailer := rw.Result().Trailer; !reflect.DeepEqual(actualTrailer, wantTrailer) {
t.Errorf("Trailer mismatch.\n got: %#v\n want: %#v", actualTrailer, wantTrailer)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/grpc/metadata"
)
// BenchmarkCreateHeaderFields measures header-slice construction as the amount
// of outgoing metadata varies.
func BenchmarkCreateHeaderFields(b *testing.B) {
for _, mdCount := range []int{0, 4, 12} {
b.Run(fmt.Sprintf("mdCount=%d", mdCount), func(b *testing.B) {
t := &http2Client{
scheme: "https",
userAgent: "grpc-go/benchmark",
md: metadata.MD{},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
if mdCount > 0 {
md := make(metadata.MD, mdCount)
for i := 0; i < mdCount; i++ {
md[fmt.Sprintf("header-%d", i)] = []string{fmt.Sprintf("value-%d", i)}
}
ctx = metadata.NewOutgoingContext(ctx, md)
}
callHdr := &CallHdr{
Method: "/grpc.testing.BenchmarkService/UnaryCall",
Host: "server:443",
}
b.ReportAllocs()
for b.Loop() {
hf, err := t.createHeaderFields(ctx, callHdr)
if err != nil {
b.Fatal(err)
}
if len(hf) == 0 {
b.Fatal("no header fields produced")
}
}
})
b.Run(fmt.Sprintf("mdCount=%d-appended", mdCount), func(b *testing.B) {
t := &http2Client{
scheme: "https",
userAgent: "grpc-go/benchmark",
md: metadata.MD{},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
kvs := make([]string, 0, mdCount*2)
for i := 0; i < mdCount; i++ {
kvs = append(kvs, fmt.Sprintf("header-%d", i), fmt.Sprintf("value-%d", i))
}
ctx = metadata.AppendToOutgoingContext(ctx, kvs...)
callHdr := &CallHdr{
Method: "/grpc.testing.BenchmarkService/UnaryCall",
Host: "server:443",
}
b.ReportAllocs()
for b.Loop() {
hf, err := t.createHeaderFields(ctx, callHdr)
if err != nil {
b.Fatal(err)
}
if len(hf) == 0 {
b.Fatal("no header fields produced")
}
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,643 @@
/*
*
* Copyright 2014 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"bufio"
"encoding/base64"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/envconfig"
imem "google.golang.org/grpc/internal/mem"
"google.golang.org/grpc/internal/transport/readyreader"
"google.golang.org/grpc/mem"
)
const (
// http2MaxFrameLen specifies the max length of a HTTP2 frame.
http2MaxFrameLen = 16384 // 16KB frame
// https://httpwg.org/specs/rfc7540.html#SettingValues
http2InitHeaderTableSize = 4096
)
var (
clientPreface = []byte(http2.ClientPreface)
http2ErrConvTab = map[http2.ErrCode]codes.Code{
http2.ErrCodeNo: codes.Internal,
http2.ErrCodeProtocol: codes.Internal,
http2.ErrCodeInternal: codes.Internal,
http2.ErrCodeFlowControl: codes.ResourceExhausted,
http2.ErrCodeSettingsTimeout: codes.Internal,
http2.ErrCodeStreamClosed: codes.Internal,
http2.ErrCodeFrameSize: codes.Internal,
http2.ErrCodeRefusedStream: codes.Unavailable,
http2.ErrCodeCancel: codes.Canceled,
http2.ErrCodeCompression: codes.Internal,
http2.ErrCodeConnect: codes.Internal,
http2.ErrCodeEnhanceYourCalm: codes.ResourceExhausted,
http2.ErrCodeInadequateSecurity: codes.PermissionDenied,
http2.ErrCodeHTTP11Required: codes.Internal,
}
// HTTPStatusConvTab is the HTTP status code to gRPC error code conversion table.
HTTPStatusConvTab = map[int]codes.Code{
// 400 Bad Request - INTERNAL.
http.StatusBadRequest: codes.Internal,
// 401 Unauthorized - UNAUTHENTICATED.
http.StatusUnauthorized: codes.Unauthenticated,
// 403 Forbidden - PERMISSION_DENIED.
http.StatusForbidden: codes.PermissionDenied,
// 404 Not Found - UNIMPLEMENTED.
http.StatusNotFound: codes.Unimplemented,
// 429 Too Many Requests - UNAVAILABLE.
http.StatusTooManyRequests: codes.Unavailable,
// 502 Bad Gateway - UNAVAILABLE.
http.StatusBadGateway: codes.Unavailable,
// 503 Service Unavailable - UNAVAILABLE.
http.StatusServiceUnavailable: codes.Unavailable,
// 504 Gateway timeout - UNAVAILABLE.
http.StatusGatewayTimeout: codes.Unavailable,
}
)
var grpcStatusDetailsBinHeader = "grpc-status-details-bin"
// isReservedHeader checks whether hdr belongs to HTTP2 headers
// reserved by gRPC protocol. Any other headers are classified as the
// user-specified metadata.
func isReservedHeader(hdr string) bool {
if hdr != "" && hdr[0] == ':' {
return true
}
switch hdr {
case "content-type",
"user-agent",
"grpc-message-type",
"grpc-encoding",
"grpc-message",
"grpc-status",
"grpc-timeout",
// Intentionally exclude grpc-previous-rpc-attempts and
// grpc-retry-pushback-ms, which are "reserved", but their API
// intentionally works via metadata.
"te":
return true
default:
return false
}
}
// isWhitelistedHeader checks whether hdr should be propagated into metadata
// visible to users, even though it is classified as "reserved", above.
func isWhitelistedHeader(hdr string) bool {
switch hdr {
case ":authority", "user-agent":
return true
default:
return false
}
}
const binHdrSuffix = "-bin"
func encodeBinHeader(v []byte) string {
return base64.RawStdEncoding.EncodeToString(v)
}
func decodeBinHeader(v string) ([]byte, error) {
if len(v)%4 == 0 {
// Input was padded, or padding was not necessary.
return base64.StdEncoding.DecodeString(v)
}
return base64.RawStdEncoding.DecodeString(v)
}
func encodeMetadataHeader(k, v string) string {
if strings.HasSuffix(k, binHdrSuffix) {
return encodeBinHeader(([]byte)(v))
}
return v
}
func decodeMetadataHeader(k, v string) (string, error) {
if strings.HasSuffix(k, binHdrSuffix) {
b, err := decodeBinHeader(v)
return string(b), err
}
return v, nil
}
type timeoutUnit uint8
const (
hour timeoutUnit = 'H'
minute timeoutUnit = 'M'
second timeoutUnit = 'S'
millisecond timeoutUnit = 'm'
microsecond timeoutUnit = 'u'
nanosecond timeoutUnit = 'n'
)
func timeoutUnitToDuration(u timeoutUnit) (d time.Duration, ok bool) {
switch u {
case hour:
return time.Hour, true
case minute:
return time.Minute, true
case second:
return time.Second, true
case millisecond:
return time.Millisecond, true
case microsecond:
return time.Microsecond, true
case nanosecond:
return time.Nanosecond, true
default:
}
return
}
func decodeTimeout(s string) (time.Duration, error) {
size := len(s)
if size < 2 {
return 0, fmt.Errorf("transport: timeout string is too short: %q", s)
}
if size > 9 {
// Spec allows for 8 digits plus the unit.
return 0, fmt.Errorf("transport: timeout string is too long: %q", s)
}
unit := timeoutUnit(s[size-1])
d, ok := timeoutUnitToDuration(unit)
if !ok {
return 0, fmt.Errorf("transport: timeout unit is not recognized: %q", s)
}
t, err := strconv.ParseUint(s[:size-1], 10, 64)
if err != nil {
return 0, err
}
const maxHours = math.MaxInt64 / uint64(time.Hour)
if d == time.Hour && t > maxHours {
// This timeout would overflow math.MaxInt64; clamp it.
return time.Duration(math.MaxInt64), nil
}
return d * time.Duration(t), nil
}
const (
spaceByte = ' '
tildeByte = '~'
percentByte = '%'
)
// encodeGrpcMessage is used to encode status code in header field
// "grpc-message". It does percent encoding and also replaces invalid utf-8
// characters with Unicode replacement character.
//
// It checks to see if each individual byte in msg is an allowable byte, and
// then either percent encoding or passing it through. When percent encoding,
// the byte is converted into hexadecimal notation with a '%' prepended.
func encodeGrpcMessage(msg string) string {
if msg == "" {
return ""
}
lenMsg := len(msg)
for i := 0; i < lenMsg; i++ {
c := msg[i]
if !(c >= spaceByte && c <= tildeByte && c != percentByte) {
return encodeGrpcMessageUnchecked(msg)
}
}
return msg
}
func encodeGrpcMessageUnchecked(msg string) string {
var sb strings.Builder
for len(msg) > 0 {
r, size := utf8.DecodeRuneInString(msg)
for _, b := range []byte(string(r)) {
if size > 1 {
// If size > 1, r is not ascii. Always do percent encoding.
fmt.Fprintf(&sb, "%%%02X", b)
continue
}
// The for loop is necessary even if size == 1. r could be
// utf8.RuneError.
//
// fmt.Sprintf("%%%02X", utf8.RuneError) gives "%FFFD".
if b >= spaceByte && b <= tildeByte && b != percentByte {
sb.WriteByte(b)
} else {
fmt.Fprintf(&sb, "%%%02X", b)
}
}
msg = msg[size:]
}
return sb.String()
}
// decodeGrpcMessage decodes the msg encoded by encodeGrpcMessage.
func decodeGrpcMessage(msg string) string {
if msg == "" {
return ""
}
lenMsg := len(msg)
for i := 0; i < lenMsg; i++ {
if msg[i] == percentByte && i+2 < lenMsg {
return decodeGrpcMessageUnchecked(msg)
}
}
return msg
}
func decodeGrpcMessageUnchecked(msg string) string {
var sb strings.Builder
lenMsg := len(msg)
for i := 0; i < lenMsg; i++ {
c := msg[i]
if c == percentByte && i+2 < lenMsg {
parsed, err := strconv.ParseUint(msg[i+1:i+3], 16, 8)
if err != nil {
sb.WriteByte(c)
} else {
sb.WriteByte(byte(parsed))
i += 2
}
} else {
sb.WriteByte(c)
}
}
return sb.String()
}
type bufWriter struct {
pool *imem.SimpleBufferPool
bufHandle *[]byte
offset int
batchSize int
conn io.Writer
err error
}
// unsharedBufWriter keeps the unshared slice header in the writer allocation.
type unsharedBufWriter struct {
bufWriter
buf []byte
}
func newBufWriter(conn io.Writer, batchSize int, pool *imem.SimpleBufferPool) *bufWriter {
if pool == nil && batchSize > 0 {
w := &unsharedBufWriter{
bufWriter: bufWriter{batchSize: batchSize, conn: conn},
buf: make([]byte, batchSize),
}
w.bufHandle = &w.buf
return &w.bufWriter
}
return &bufWriter{batchSize: batchSize, conn: conn, pool: pool}
}
func (w *bufWriter) Write(b []byte) (int, error) {
if w.err != nil {
return 0, w.err
}
if w.batchSize == 0 { // Buffer has been disabled.
n, err := w.conn.Write(b)
return n, toIOError(err)
}
if w.bufHandle == nil {
w.bufHandle = w.pool.Get(w.batchSize)
}
buf := *w.bufHandle
written := 0
for len(b) > 0 {
copied := copy(buf[w.offset:], b)
b = b[copied:]
written += copied
w.offset += copied
if w.offset < w.batchSize {
continue
}
if err := w.flushKeepBuffer(); err != nil {
return written, err
}
}
return written, nil
}
func (w *bufWriter) Flush() error {
err := w.flushKeepBuffer()
w.releaseBuffer()
return err
}
func (w *bufWriter) releaseBuffer() {
if w.pool != nil && w.bufHandle != nil {
w.pool.Put(w.bufHandle)
w.bufHandle = nil
}
}
func (w *bufWriter) flushKeepBuffer() error {
if w.err != nil {
return w.err
}
if w.offset == 0 {
return nil
}
buf := *w.bufHandle
_, w.err = w.conn.Write(buf[:w.offset])
w.err = toIOError(w.err)
w.offset = 0
if w.err != nil {
w.releaseBuffer()
}
return w.err
}
type ioError struct {
error
}
func (i ioError) Unwrap() error {
return i.error
}
func isIOError(err error) bool {
return errors.As(err, &ioError{})
}
func toIOError(err error) error {
if err == nil {
return nil
}
return ioError{error: err}
}
type parsedDataFrame struct {
http2.FrameHeader
data mem.Buffer
}
func (df *parsedDataFrame) StreamEnded() bool {
return df.FrameHeader.Flags.Has(http2.FlagDataEndStream)
}
type framer struct {
writer *bufWriter
fr *http2.Framer
headerBuf []byte // cached slice for framer headers to reduce heap allocs.
reader io.Reader
dataFrame parsedDataFrame // Cached data frame to avoid heap allocations.
pool mem.BufferPool
errDetail error
}
var ioBufferPoolMap = make(map[int]*imem.SimpleBufferPool)
var ioBufferMutex sync.Mutex
func bufferedReader(r io.Reader, bufSize int) io.Reader {
if bufSize <= 0 {
return r
}
if envconfig.EnableHTTPFramerReadBufferPooling {
if rr := readyreader.NewNonBlocking(r); rr != nil {
readPool := ioBufferPool(bufSize)
return readyreader.NewBuffered(rr, bufSize, readPool)
}
}
return bufio.NewReaderSize(r, bufSize)
}
func newFramer(conn io.ReadWriter, writeBufferSize, readBufferSize int, sharedWriteBuffer bool, maxHeaderListSize uint32, memPool mem.BufferPool) *framer {
if writeBufferSize < 0 {
writeBufferSize = 0
}
r := bufferedReader(conn, readBufferSize)
var writePool *imem.SimpleBufferPool
if sharedWriteBuffer {
writePool = ioBufferPool(writeBufferSize)
}
w := newBufWriter(conn, writeBufferSize, writePool)
f := &framer{
writer: w,
fr: http2.NewFramer(w, r),
reader: r,
pool: memPool,
}
f.fr.SetMaxReadFrameSize(http2MaxFrameLen)
// Opt-in to Frame reuse API on framer to reduce garbage.
// Frames aren't safe to read from after a subsequent call to ReadFrame.
f.fr.SetReuseFrames()
f.fr.MaxHeaderListSize = maxHeaderListSize
f.fr.ReadMetaHeaders = hpack.NewDecoder(http2InitHeaderTableSize, nil)
return f
}
// writeData writes a DATA frame.
//
// It is the caller's responsibility not to violate the maximum frame size.
func (f *framer) writeData(streamID uint32, endStream bool, data [][]byte) error {
var flags http2.Flags
if endStream {
flags = http2.FlagDataEndStream
}
length := uint32(0)
for _, d := range data {
length += uint32(len(d))
}
// TODO: Replace the header write with the framer API being added in
// https://github.com/golang/go/issues/66655.
f.headerBuf = append(f.headerBuf[:0],
byte(length>>16),
byte(length>>8),
byte(length),
byte(http2.FrameData),
byte(flags),
byte(streamID>>24),
byte(streamID>>16),
byte(streamID>>8),
byte(streamID))
if _, err := f.writer.Write(f.headerBuf); err != nil {
return err
}
for _, d := range data {
if _, err := f.writer.Write(d); err != nil {
return err
}
}
return nil
}
// readFrame reads a single frame. The returned Frame is only valid
// until the next call to readFrame.
func (f *framer) readFrame() (any, error) {
f.errDetail = nil
fh, err := f.fr.ReadFrameHeader()
if err != nil {
f.errDetail = f.fr.ErrorDetail()
return nil, err
}
// Read the data frame directly from the underlying io.Reader to avoid
// copies.
if fh.Type == http2.FrameData {
err = f.readDataFrame(fh)
return &f.dataFrame, err
}
fr, err := f.fr.ReadFrameForHeader(fh)
if err != nil {
f.errDetail = f.fr.ErrorDetail()
return nil, err
}
return fr, err
}
// errorDetail returns a more detailed error of the last error
// returned by framer.readFrame. For instance, if readFrame
// returns a StreamError with code PROTOCOL_ERROR, errorDetail
// will say exactly what was invalid. errorDetail is not guaranteed
// to return a non-nil value.
// errorDetail is reset after the next call to readFrame.
func (f *framer) errorDetail() error {
return f.errDetail
}
func (f *framer) readDataFrame(fh http2.FrameHeader) (err error) {
if fh.StreamID == 0 {
// DATA frames MUST be associated with a stream. If a
// DATA frame is received whose stream identifier
// field is 0x0, the recipient MUST respond with a
// connection error (Section 5.4.1) of type
// PROTOCOL_ERROR.
f.errDetail = errors.New("DATA frame with stream ID 0")
return http2.ConnectionError(http2.ErrCodeProtocol)
}
// Converting a *[]byte to a mem.SliceBuffer incurs a heap allocation. This
// conversion is performed by mem.NewBuffer. To avoid the extra allocation
// a []byte is allocated directly if required and cast to a mem.SliceBuffer.
var buf []byte
// poolHandle is the pointer returned by the buffer pool (if it's used.).
var poolHandle *[]byte
useBufferPool := !mem.IsBelowBufferPoolingThreshold(int(fh.Length))
if useBufferPool {
poolHandle = f.pool.Get(int(fh.Length))
buf = *poolHandle
defer func() {
if err != nil {
f.pool.Put(poolHandle)
}
}()
} else {
buf = make([]byte, int(fh.Length))
}
if fh.Flags.Has(http2.FlagDataPadded) {
if fh.Length == 0 {
return io.ErrUnexpectedEOF
}
// This initial 1-byte read can be inefficient for unbuffered readers,
// but it allows the rest of the payload to be read directly to the
// start of the destination slice. This makes it easy to return the
// original slice back to the buffer pool.
if _, err := io.ReadFull(f.reader, buf[:1]); err != nil {
return err
}
padSize := buf[0]
buf = buf[:len(buf)-1]
if int(padSize) > len(buf) {
// If the length of the padding is greater than the
// length of the frame payload, the recipient MUST
// treat this as a connection error.
// Filed: https://github.com/http2/http2-spec/issues/610
f.errDetail = errors.New("pad size larger than data payload")
return http2.ConnectionError(http2.ErrCodeProtocol)
}
if _, err := io.ReadFull(f.reader, buf); err != nil {
return err
}
buf = buf[:len(buf)-int(padSize)]
} else if _, err := io.ReadFull(f.reader, buf); err != nil {
return err
}
f.dataFrame.FrameHeader = fh
if useBufferPool {
// Update the handle to point to the (potentially re-sliced) buf.
*poolHandle = buf
f.dataFrame.data = mem.NewBuffer(poolHandle, f.pool)
} else {
f.dataFrame.data = mem.SliceBuffer(buf)
}
return nil
}
func (df *parsedDataFrame) Header() http2.FrameHeader {
return df.FrameHeader
}
func ioBufferPool(size int) *imem.SimpleBufferPool {
ioBufferMutex.Lock()
defer ioBufferMutex.Unlock()
pool, ok := ioBufferPoolMap[size]
if ok {
return pool
}
pool = imem.NewDirtySimplePool()
ioBufferPoolMap[size] = pool
return pool
}
// ParseDialTarget returns the network and address to pass to dialer.
func ParseDialTarget(target string) (string, string) {
net := "tcp"
m1 := strings.Index(target, ":")
m2 := strings.Index(target, ":/")
// handle unix:addr which will fail with url.Parse
if m1 >= 0 && m2 < 0 {
if n := target[0:m1]; n == "unix" {
return n, target[m1+1:]
}
}
if m2 >= 0 {
t, err := url.Parse(target)
if err != nil {
return net, target
}
scheme := t.Scheme
addr := t.Path
if scheme == "unix" {
if addr == "" {
addr = t.Host
}
return scheme, addr
}
}
return net, target
}

View File

@@ -0,0 +1,637 @@
/*
*
* Copyright 2014 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"math"
"net"
"reflect"
"strings"
"testing"
"time"
"golang.org/x/net/http2"
"google.golang.org/grpc/internal/envconfig"
imem "google.golang.org/grpc/internal/mem"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/transport/readyreader"
"google.golang.org/grpc/mem"
)
func (s) TestDecodeTimeout(t *testing.T) {
for _, test := range []struct {
// input
s string
// output
d time.Duration
wantErr bool
}{
{"00000001n", time.Nanosecond, false},
{"10u", time.Microsecond * 10, false},
{"00000010m", time.Millisecond * 10, false},
{"1234S", time.Second * 1234, false},
{"00000001M", time.Minute, false},
{"09999999S", time.Second * 9999999, false},
{"99999999S", time.Second * 99999999, false},
{"99999999M", time.Minute * 99999999, false},
{"2562047H", time.Hour * 2562047, false},
{"2562048H", time.Duration(math.MaxInt64), false},
{"99999999H", time.Duration(math.MaxInt64), false},
{"-1S", 0, true},
{"1234x", 0, true},
{"1234s", 0, true},
{"1234", 0, true},
{"1", 0, true},
{"", 0, true},
{"9a1S", 0, true},
{"0S", 0, false}, // PROTOCOL-HTTP2.md requires positive integers, but we allow it to timeout instead
{"00000000S", 0, false},
{"000000000S", 0, true}, // PROTOCOL-HTTP2.md allows at most 8 digits
} {
d, err := decodeTimeout(test.s)
gotErr := err != nil
if d != test.d || gotErr != test.wantErr {
t.Errorf("timeoutDecode(%q) = %d, %v, want %d, wantErr=%v",
test.s, int64(d), err, int64(test.d), test.wantErr)
}
}
}
func (s) TestEncodeGrpcMessage(t *testing.T) {
for _, tt := range []struct {
input string
expected string
}{
{"", ""},
{"Hello", "Hello"},
{"\u0000", "%00"},
{"%", "%25"},
{"系统", "%E7%B3%BB%E7%BB%9F"},
{string([]byte{0xff, 0xfe, 0xfd}), "%EF%BF%BD%EF%BF%BD%EF%BF%BD"},
} {
actual := encodeGrpcMessage(tt.input)
if tt.expected != actual {
t.Errorf("encodeGrpcMessage(%q) = %q, want %q", tt.input, actual, tt.expected)
}
}
// make sure that all the visible ASCII chars except '%' are not percent encoded.
for i := ' '; i <= '~' && i != '%'; i++ {
output := encodeGrpcMessage(string(i))
if output != string(i) {
t.Errorf("encodeGrpcMessage(%v) = %v, want %v", string(i), output, string(i))
}
}
// make sure that all the invisible ASCII chars and '%' are percent encoded.
for i := rune(0); i == '%' || (i >= rune(0) && i < ' ') || (i > '~' && i <= rune(127)); i++ {
output := encodeGrpcMessage(string(i))
expected := fmt.Sprintf("%%%02X", i)
if output != expected {
t.Errorf("encodeGrpcMessage(%v) = %v, want %v", string(i), output, expected)
}
}
}
func (s) TestDecodeGrpcMessage(t *testing.T) {
for _, tt := range []struct {
input string
expected string
}{
{"", ""},
{"Hello", "Hello"},
{"H%61o", "Hao"},
{"H%6", "H%6"},
{"%G0", "%G0"},
{"%E7%B3%BB%E7%BB%9F", "系统"},
{"%EF%BF%BD", "<22>"},
} {
actual := decodeGrpcMessage(tt.input)
if tt.expected != actual {
t.Errorf("decodeGrpcMessage(%q) = %q, want %q", tt.input, actual, tt.expected)
}
}
// make sure that all the visible ASCII chars except '%' are not percent decoded.
for i := ' '; i <= '~' && i != '%'; i++ {
output := decodeGrpcMessage(string(i))
if output != string(i) {
t.Errorf("decodeGrpcMessage(%v) = %v, want %v", string(i), output, string(i))
}
}
// make sure that all the invisible ASCII chars and '%' are percent decoded.
for i := rune(0); i == '%' || (i >= rune(0) && i < ' ') || (i > '~' && i <= rune(127)); i++ {
output := decodeGrpcMessage(fmt.Sprintf("%%%02X", i))
if output != string(i) {
t.Errorf("decodeGrpcMessage(%v) = %v, want %v", fmt.Sprintf("%%%02X", i), output, string(i))
}
}
}
// Decode an encoded string should get the same thing back, except for invalid
// utf8 chars.
func (s) TestDecodeEncodeGrpcMessage(t *testing.T) {
testCases := []struct {
orig string
want string
}{
{"", ""},
{"hello", "hello"},
{"h%6", "h%6"},
{"%G0", "%G0"},
{"系统", "系统"},
{"Hello, 世界", "Hello, 世界"},
{string([]byte{0xff, 0xfe, 0xfd}), "<22><><EFBFBD>"},
{string([]byte{0xff}) + "Hello" + string([]byte{0xfe}) + "世界" + string([]byte{0xfd}), "<22>Hello<6C>世界<E4B896>"},
}
for _, tC := range testCases {
got := decodeGrpcMessage(encodeGrpcMessage(tC.orig))
if got != tC.want {
t.Errorf("decodeGrpcMessage(encodeGrpcMessage(%q)) = %q, want %q", tC.orig, got, tC.want)
}
}
}
const binaryValue = "\u0080"
func (s) TestEncodeMetadataHeader(t *testing.T) {
for _, test := range []struct {
// input
kin string
vin string
// output
vout string
}{
{"key", "abc", "abc"},
{"KEY", "abc", "abc"},
{"key-bin", "abc", "YWJj"},
{"key-bin", binaryValue, "woA"},
} {
v := encodeMetadataHeader(test.kin, test.vin)
if !reflect.DeepEqual(v, test.vout) {
t.Fatalf("encodeMetadataHeader(%q, %q) = %q, want %q", test.kin, test.vin, v, test.vout)
}
}
}
func (s) TestDecodeMetadataHeader(t *testing.T) {
for _, test := range []struct {
// input
kin string
vin string
// output
vout string
err error
}{
{"a", "abc", "abc", nil},
{"key-bin", "Zm9vAGJhcg==", "foo\x00bar", nil},
{"key-bin", "Zm9vAGJhcg", "foo\x00bar", nil},
{"key-bin", "woA=", binaryValue, nil},
{"a", "abc,efg", "abc,efg", nil},
} {
v, err := decodeMetadataHeader(test.kin, test.vin)
if !reflect.DeepEqual(v, test.vout) || !reflect.DeepEqual(err, test.err) {
t.Fatalf("decodeMetadataHeader(%q, %q) = %q, %v, want %q, %v", test.kin, test.vin, v, err, test.vout, test.err)
}
}
}
func (s) TestParseDialTarget(t *testing.T) {
for _, test := range []struct {
target, wantNet, wantAddr string
}{
{"unix:a", "unix", "a"},
{"unix:a/b/c", "unix", "a/b/c"},
{"unix:/a", "unix", "/a"},
{"unix:/a/b/c", "unix", "/a/b/c"},
{"unix://a", "unix", "a"},
{"unix://a/b/c", "unix", "/b/c"},
{"unix:///a", "unix", "/a"},
{"unix:///a/b/c", "unix", "/a/b/c"},
{"unix:etcd:0", "unix", "etcd:0"},
{"unix:///tmp/unix-3", "unix", "/tmp/unix-3"},
{"unix://domain", "unix", "domain"},
{"unix://etcd:0", "unix", "etcd:0"},
{"unix:///etcd:0", "unix", "/etcd:0"},
{"passthrough://unix://domain", "tcp", "passthrough://unix://domain"},
{"https://google.com:443", "tcp", "https://google.com:443"},
{"dns:///google.com", "tcp", "dns:///google.com"},
{"/unix/socket/address", "tcp", "/unix/socket/address"},
} {
gotNet, gotAddr := ParseDialTarget(test.target)
if gotNet != test.wantNet || gotAddr != test.wantAddr {
t.Errorf("ParseDialTarget(%q) = %s, %s want %s, %s", test.target, gotNet, gotAddr, test.wantNet, test.wantAddr)
}
}
}
type badNetworkConn struct {
net.Conn
}
func (c *badNetworkConn) Write([]byte) (int, error) {
return 0, io.EOF
}
// This test ensures Write() on a broken network connection does not lead to
// an infinite loop. See https://github.com/grpc/grpc-go/issues/7389 for more details.
func (s) TestWriteBadConnection(t *testing.T) {
data := []byte("test_data")
// Configure the bufWriter with a batchsize that results in data being flushed
// to the underlying conn, midway through Write().
writeBufferSize := (len(data) - 1) / 2
writer := newBufWriter(&badNetworkConn{}, writeBufferSize, ioBufferPool(writeBufferSize))
errCh := make(chan error, 1)
go func() {
_, err := writer.Write(data)
errCh <- err
}()
select {
case <-time.After(time.Second):
t.Fatalf("Write() did not return in time")
case err := <-errCh:
if !errors.Is(err, io.EOF) {
t.Fatalf("Write() = %v, want error presence = %v", err, io.EOF)
}
}
if writer.bufHandle != nil {
t.Fatalf("failed Write() retained pooled buffer: handle=%p", writer.bufHandle)
}
}
func (s) TestBufWriterSharedFlushAllocations(t *testing.T) {
pool := imem.NewDirtySimplePool()
writer := newBufWriter(io.Discard, defaultWriteBufSize, pool)
payload := make([]byte, 128)
allocs := testing.AllocsPerRun(1000, func() {
if _, err := writer.Write(payload); err != nil {
t.Fatalf("Write() failed: %v", err)
}
if err := writer.Flush(); err != nil {
t.Fatalf("Flush() failed: %v", err)
}
})
if allocs != 0 {
t.Fatalf("shared Write()+Flush() allocated %v times, want 0", allocs)
}
}
func (s) TestBufWriterSharedBufferOwnership(t *testing.T) {
const batchSize = 8
pool := imem.NewDirtySimplePool()
var conn bytes.Buffer
writer := newBufWriter(&conn, batchSize, pool)
if _, err := writer.Write([]byte("abc")); err != nil {
t.Fatalf("Write() failed: %v", err)
}
if writer.bufHandle == nil {
t.Fatal("Write() did not retain the pooled buffer handle")
}
if err := writer.Flush(); err != nil {
t.Fatalf("Flush() failed: %v", err)
}
if writer.bufHandle != nil {
t.Fatalf("Flush() retained pooled buffer: handle=%p", writer.bufHandle)
}
if got := conn.String(); got != "abc" {
t.Fatalf("conn data = %q, want %q", got, "abc")
}
if err := writer.Flush(); err != nil {
t.Fatalf("second Flush() failed: %v", err)
}
if _, err := writer.Write([]byte("12345678")); err != nil {
t.Fatalf("full-buffer Write() failed: %v", err)
}
if writer.bufHandle == nil {
t.Fatal("full-buffer Write() released the handle during its internal flush")
}
if writer.offset != 0 {
t.Fatalf("full-buffer Write() offset = %d, want 0", writer.offset)
}
if err := writer.Flush(); err != nil {
t.Fatalf("Flush() after full-buffer Write() failed: %v", err)
}
if writer.bufHandle != nil {
t.Fatalf("Flush() retained pooled buffer: handle=%p", writer.bufHandle)
}
}
func (s) TestBufWriterFlushErrorReleasesSharedBuffer(t *testing.T) {
pool := imem.NewDirtySimplePool()
writer := newBufWriter(&badNetworkConn{}, 8, pool)
if _, err := writer.Write([]byte("abc")); err != nil {
t.Fatalf("Write() failed before flush: %v", err)
}
if writer.bufHandle == nil {
t.Fatal("Write() did not retain the pooled buffer handle")
}
if err := writer.Flush(); !errors.Is(err, io.EOF) {
t.Fatalf("Flush() error = %v, want %v", err, io.EOF)
}
if writer.bufHandle != nil {
t.Fatalf("failed Flush() retained pooled buffer: handle=%p", writer.bufHandle)
}
if _, err := writer.Write([]byte("def")); !errors.Is(err, io.EOF) {
t.Fatalf("Write() after failed Flush() error = %v, want %v", err, io.EOF)
}
if writer.bufHandle != nil {
t.Fatal("Write() after failed Flush() acquired another pooled buffer")
}
}
func BenchmarkBufWriter(b *testing.B) {
const payloadSize = 128
payload := make([]byte, payloadSize)
for _, test := range []struct {
name string
pool *imem.SimpleBufferPool
}{
{name: "Shared", pool: imem.NewDirtySimplePool()},
{name: "Unshared"},
} {
b.Run(test.name, func(b *testing.B) {
b.Run("New", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
_ = writer
}
})
b.Run("WriteAndFlush", func(b *testing.B) {
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
b.ReportAllocs()
b.SetBytes(payloadSize)
for b.Loop() {
if _, err := writer.Write(payload); err != nil {
b.Fatal(err)
}
if err := writer.Flush(); err != nil {
b.Fatal(err)
}
}
})
b.Run("EmptyFlush", func(b *testing.B) {
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
b.ReportAllocs()
for b.Loop() {
if err := writer.Flush(); err != nil {
b.Fatal(err)
}
}
})
b.Run("FullBufferWrite", func(b *testing.B) {
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
fullBuffer := make([]byte, defaultWriteBufSize)
b.ReportAllocs()
b.SetBytes(defaultWriteBufSize)
for b.Loop() {
if _, err := writer.Write(fullBuffer); err != nil {
b.Fatal(err)
}
}
})
})
}
}
func BenchmarkDecodeGrpcMessage(b *testing.B) {
input := "Hello, %E4%B8%96%E7%95%8C"
want := "Hello, 世界"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
got := decodeGrpcMessage(input)
if got != want {
b.Fatalf("decodeGrpcMessage(%q) = %s, want %s", input, got, want)
}
}
}
func BenchmarkEncodeGrpcMessage(b *testing.B) {
input := "Hello, 世界"
want := "Hello, %E4%B8%96%E7%95%8C"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
got := encodeGrpcMessage(input)
if got != want {
b.Fatalf("encodeGrpcMessage(%q) = %s, want %s", input, got, want)
}
}
}
func buildDataFrame(h http2.FrameHeader, payload []byte) []byte {
buf := new(bytes.Buffer)
buf.Write([]byte{
byte(h.Length >> 16),
byte(h.Length >> 8),
byte(h.Length),
byte(h.Type),
byte(h.Flags),
byte(h.StreamID >> 24),
byte(h.StreamID >> 16),
byte(h.StreamID >> 8),
byte(h.StreamID),
})
buf.Write(payload)
return buf.Bytes()
}
func (s) TestFramer_ParseDataFrame(t *testing.T) {
tests := []struct {
name string
wire []byte // from frame header onward
wantData []byte
wantErr error
wantErrDetailSubstr string
}{
{
name: "good_padded",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 6, StreamID: 1, Flags: http2.FlagDataPadded,
}, []byte{
2, // pad length
'f', 'o', 'o', // data
0, 0, // padding
}),
wantData: []byte("foo"),
},
{
name: "good_unpadded",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 3, StreamID: 1, Flags: 0,
}, []byte("foo")),
wantData: []byte("foo"),
},
{
name: "stream_id_0",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 1, StreamID: 0, Flags: 0,
}, []byte{0}),
wantErr: http2.ConnectionError(http2.ErrCodeProtocol),
wantErrDetailSubstr: "DATA frame with stream ID 0",
},
{
name: "pad_size_bigger_than_payload",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 4, StreamID: 1, Flags: http2.FlagDataPadded,
}, []byte{
4, // pad length of 4
'f', 'o', // data 'fo' is 2 bytes
0, // padding 0 is 1 byte.
}), // pad length 4 but only 3 bytes for data+padding available in payload after pad length byte
wantErr: http2.ConnectionError(http2.ErrCodeProtocol),
wantErrDetailSubstr: "pad size larger than data payload",
},
{
name: "padded_zero_data_some_padding",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 3, StreamID: 1, Flags: http2.FlagDataPadded,
}, []byte{
2, // pad length 2
0, 0, // padding
}),
wantData: []byte{},
},
{
name: "padded_short_payload_reading_pad_flag",
wire: buildDataFrame(http2.FrameHeader{
Type: http2.FrameData, Length: 0, StreamID: 1, Flags: http2.FlagDataPadded,
}, []byte{}),
wantErr: io.ErrUnexpectedEOF,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fr := newFramer(bytes.NewBuffer(tc.wire), defaultWriteBufSize, defaultReadBufSize, false, defaultClientMaxHeaderListSize, mem.DefaultBufferPool())
f, err := fr.readFrame()
if err != tc.wantErr {
t.Fatalf("readFrame() returned unexpected error: %v, want %v", err, tc.wantErr)
}
gotErrDetailStr := ""
if fr.errDetail != nil {
gotErrDetailStr = fr.errDetail.Error()
}
if !strings.Contains(gotErrDetailStr, tc.wantErrDetailSubstr) {
t.Fatalf("errorDetail() returned unexpected error string: %q, want substring %q", gotErrDetailStr, tc.wantErrDetailSubstr)
}
if tc.wantErr != nil {
return
}
df, ok := f.(*parsedDataFrame)
if !ok {
t.Fatalf("readFrame() returned %T, want *parsedDataFrame", f)
}
if gotData := df.data.ReadOnlyData(); !bytes.Equal(gotData, tc.wantData) {
t.Fatalf("parsedDataFrame.Data() = %q, want %q", gotData, tc.wantData)
}
df.data.Free()
})
}
}
type testReadyReader struct {
readyreader.Reader
}
func (t *testReadyReader) Read([]byte) (int, error) {
return 0, io.EOF
}
func (s) TestBufferedReader(t *testing.T) {
normalReader := bytes.NewReader(nil)
tests := []struct {
name string
reader io.Reader
bufSize int
enablePooling bool
wantTypeOf any
}{
{
name: "bufSize_0",
reader: normalReader,
bufSize: 0,
enablePooling: true,
wantTypeOf: (*bytes.Reader)(nil),
},
{
name: "env_var_disabled_normal_reader",
reader: normalReader,
bufSize: 10,
enablePooling: false,
wantTypeOf: (*bufio.Reader)(nil),
},
{
name: "env_var_disabled_ready_reader",
reader: &testReadyReader{},
bufSize: 10,
enablePooling: false,
wantTypeOf: (*bufio.Reader)(nil),
},
{
name: "env_var_enabled_normal_reader",
reader: normalReader,
bufSize: 10,
enablePooling: true,
wantTypeOf: (*bufio.Reader)(nil),
},
{
name: "env_var_enabled_ready_reader",
reader: &testReadyReader{},
bufSize: 10,
enablePooling: true,
wantTypeOf: readyreader.NewBuffered(nil, 10, mem.DefaultBufferPool()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testutils.SetEnvConfig(t, &envconfig.EnableHTTPFramerReadBufferPooling, tt.enablePooling)
got := bufferedReader(tt.reader, tt.bufSize)
gotType := reflect.TypeOf(got)
wantType := reflect.TypeOf(tt.wantTypeOf)
if gotType != wantType {
t.Errorf("bufferedReader() type = %v, want %v", gotType, wantType)
}
})
}
}

View File

@@ -0,0 +1,25 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package internal contains functionality internal to the transport package.
package internal
// TimeNowFunc is a variable that can be set to override the default behavior of
// getting the current time in nanoseconds. It is used in transport code to set
// channelz timestamps, and is exposed here for testing purposes.
var TimeNowFunc func() int64

View File

@@ -0,0 +1,860 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// This file contains tests related to the following proposals:
// https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md
// https://github.com/grpc/proposal/blob/master/A9-server-side-conn-mgt.md
// https://github.com/grpc/proposal/blob/master/A18-tcp-user-timeout.md
package transport
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net"
"os"
"strings"
"testing"
"time"
"golang.org/x/net/http2"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/channelz"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/syscall"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/testdata"
)
const defaultTestTimeout = 10 * time.Second
const defaultTestShortTimeout = 10 * time.Millisecond
// TestMaxConnectionIdle tests that a server will send GoAway to an idle
// client. An idle client is one who doesn't make any RPC calls for a duration
// of MaxConnectionIdle time.
func (s) TestMaxConnectionIdle(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ServerParameters{
MaxConnectionIdle: 30 * time.Millisecond,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
if err != nil {
t.Fatalf("client.NewStream() failed: %v", err)
}
stream.Close(io.EOF)
// Verify the server sends a GoAway to client after MaxConnectionIdle timeout
// kicks in.
select {
case <-ctx.Done():
t.Fatalf("context expired before receiving GoAway from the server.")
case <-client.GoAway():
reason, debugMsg := client.GetGoAwayReason()
if reason != GoAwayNoReason {
t.Fatalf("GoAwayReason is %v, want %v", reason, GoAwayNoReason)
}
if !strings.Contains(debugMsg, "max_idle") {
t.Fatalf("GoAwayDebugMessage is %v, want %v", debugMsg, "max_idle")
}
}
}
// TestMaxConnectionIdleBusyClient tests that a server will not send GoAway to
// a busy client.
func (s) TestMaxConnectionIdleBusyClient(t *testing.T) {
serverConfig := &ServerConfig{
KeepaliveParams: keepalive.ServerParameters{
MaxConnectionIdle: 100 * time.Millisecond,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, err := client.NewStream(ctx, &CallHdr{}, nil)
if err != nil {
t.Fatalf("client.NewStream() failed: %v", err)
}
// Verify the server does not send a GoAway to client even after MaxConnectionIdle
// timeout kicks in.
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case <-client.GoAway():
t.Fatalf("A busy client received a GoAway.")
case <-ctx.Done():
}
}
// TestMaxConnectionAge tests that a server will send GoAway after a duration
// of MaxConnectionAge.
func (s) TestMaxConnectionAge(t *testing.T) {
maxConnAge := 100 * time.Millisecond
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ServerParameters{
MaxConnectionAge: maxConnAge,
MaxConnectionAgeGrace: 10 * time.Millisecond,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
t.Fatalf("client.NewStream() failed: %v", err)
}
// Verify the server sends a GoAway to client even after client remains idle
// for more than MaxConnectionIdle time.
select {
case <-client.GoAway():
reason, debugMsg := client.GetGoAwayReason()
if reason != GoAwayNoReason {
t.Fatalf("GoAwayReason is %v, want %v", reason, GoAwayNoReason)
}
if !strings.Contains(debugMsg, "max_age") {
t.Fatalf("GoAwayDebugMessage is %v, want %v", debugMsg, "max_age")
}
case <-ctx.Done():
t.Fatalf("timed out before getting a GoAway from the server.")
}
}
const (
defaultWriteBufSize = 32 * 1024
defaultReadBufSize = 32 * 1024
)
// TestKeepaliveServerClosesUnresponsiveClient tests that a server closes
// the connection with a client that doesn't respond to keepalive pings.
//
// This test creates a regular net.Conn connection to the server and sends the
// clientPreface and the initial Settings frame, and then remains unresponsive.
func (s) TestKeepaliveServerClosesUnresponsiveClient(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ServerParameters{
Time: 100 * time.Millisecond,
Timeout: 10 * time.Millisecond,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
addr := server.addr()
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("net.Dial(tcp, %v) failed: %v", addr, err)
}
defer conn.Close()
if n, err := conn.Write(clientPreface); err != nil || n != len(clientPreface) {
t.Fatalf("conn.Write(clientPreface) failed: n=%v, err=%v", n, err)
}
framer := newFramer(conn, defaultWriteBufSize, defaultReadBufSize, false, 0, mem.DefaultBufferPool())
if err := framer.fr.WriteSettings(http2.Setting{}); err != nil {
t.Fatal("framer.WriteSettings(http2.Setting{}) failed:", err)
}
framer.writer.Flush()
// We read from the net.Conn till we get an error, which is expected when
// the server closes the connection as part of the keepalive logic.
errCh := make(chan error, 1)
go func() {
b := make([]byte, 24)
for {
if _, err = conn.Read(b); err != nil {
errCh <- err
return
}
}
}()
// Server waits for KeepaliveParams.Time seconds before sending out a ping,
// and then waits for KeepaliveParams.Timeout for a ping ack.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
select {
case err := <-errCh:
if err != io.EOF {
t.Fatalf("client.Read(_) = _,%v, want io.EOF", err)
}
case <-ctx.Done():
t.Fatalf("Test timed out before server closed the connection.")
}
}
// TestKeepaliveServerWithResponsiveClient tests that a server doesn't close
// the connection with a client that responds to keepalive pings.
func (s) TestKeepaliveServerWithResponsiveClient(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ServerParameters{
Time: 100 * time.Millisecond,
Timeout: 100 * time.Millisecond,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
// Give keepalive logic some time by sleeping.
time.Sleep(500 * time.Millisecond)
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
func channelzSubChannel(t *testing.T) *channelz.SubChannel {
ch := channelz.RegisterChannel(nil, "test chan")
sc := channelz.RegisterSubChannel(ch, "test subchan")
t.Cleanup(func() {
channelz.RemoveEntry(sc.ID)
channelz.RemoveEntry(ch.ID)
})
return sc
}
// TestKeepaliveClientClosesUnresponsiveServer creates a server which does not
// respond to keepalive pings, and makes sure that the client closes the
// transport once the keepalive logic kicks in. Here, we set the
// `PermitWithoutStream` parameter to true which ensures that the keepalive
// logic is running even without any active streams.
func (s) TestKeepaliveClientClosesUnresponsiveServer(t *testing.T) {
connCh := make(chan net.Conn, 1)
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
ChannelzParent: channelzSubChannel(t),
KeepaliveParams: keepalive.ClientParameters{
Time: 10 * time.Millisecond,
Timeout: 10 * time.Millisecond,
PermitWithoutStream: true,
},
}
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
server.setPingAck(false)
defer cancel()
defer client.Close(fmt.Errorf("closed manually by test"))
conn, ok := <-connCh
if !ok {
t.Fatalf("Server didn't return connection object")
}
defer conn.Close()
if err := pollForStreamCreationError(client); err != nil {
t.Fatal(err)
}
}
// TestKeepaliveClientOpenWithUnresponsiveServer creates a server which does
// not respond to keepalive pings, and makes sure that the client does not
// close the transport. Here, we do not set the `PermitWithoutStream` parameter
// to true which ensures that the keepalive logic is turned off without any
// active streams, and therefore the transport stays open.
func (s) TestKeepaliveClientOpenWithUnresponsiveServer(t *testing.T) {
connCh := make(chan net.Conn, 1)
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
ChannelzParent: channelzSubChannel(t),
KeepaliveParams: keepalive.ClientParameters{
Time: 10 * time.Millisecond,
Timeout: 10 * time.Millisecond,
},
}
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
server.setPingAck(false)
defer cancel()
defer client.Close(fmt.Errorf("closed manually by test"))
conn, ok := <-connCh
if !ok {
t.Fatalf("Server didn't return connection object")
}
defer conn.Close()
// Give keepalive some time.
time.Sleep(500 * time.Millisecond)
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
// TestKeepaliveClientClosesWithActiveStreams creates a responsive server and
// then stops responding to keepalive pings. It makes sure that the client
// closes the transport even when there is an active stream.
func (s) TestKeepaliveClientClosesWithActiveStreams(t *testing.T) {
connCh := make(chan net.Conn, 1)
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
ChannelzParent: channelzSubChannel(t),
KeepaliveParams: keepalive.ClientParameters{
Time: 10 * time.Millisecond,
Timeout: 100 * time.Millisecond,
},
}
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
defer cancel()
defer client.Close(fmt.Errorf("closed manually by test"))
conn, ok := <-connCh
if !ok {
t.Fatalf("Server didn't return connection object")
}
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a stream, but send no data on it.
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
// Now that we have an active stream and verified the connection is stable,
// we want to simulate a "no-ping" server.
server.setPingAck(false)
if err := pollForStreamCreationError(client); err != nil {
t.Fatal(err)
}
}
// TestKeepaliveClientStaysHealthyWithResponsiveServer creates a server which
// responds to keepalive pings, and makes sure than a client transport stays
// healthy without any active streams.
func (s) TestKeepaliveClientStaysHealthyWithResponsiveServer(t *testing.T) {
server, client, cancel := setUpWithOptions(t, 0,
&ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: 50 * time.Millisecond,
PermitWithoutStream: true,
},
},
normal,
ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 55 * time.Millisecond,
Timeout: time.Second,
PermitWithoutStream: true,
}})
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
// Give keepalive some time.
time.Sleep(500 * time.Millisecond)
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
// TestKeepaliveClientFrequency creates a server which expects at most 1 client
// ping for every 100 ms, while the client is configured to send a ping
// every 50 ms. So, this configuration should end up with the client
// transport being closed. But we had a bug wherein the client was sending one
// ping every [Time+Timeout] instead of every [Time] period, and this test
// explicitly makes sure the fix works and the client sends a ping every [Time]
// period.
func (s) TestKeepaliveClientFrequency(t *testing.T) {
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: 100 * time.Millisecond,
PermitWithoutStream: true,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 50 * time.Millisecond,
Timeout: time.Second,
PermitWithoutStream: true,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
if err := waitForGoAwayTooManyPings(client); err != nil {
t.Fatal(err)
}
}
// TestKeepaliveServerEnforcementWithAbusiveClientNoRPC verifies that the
// server closes a client transport when it sends too many keepalive pings
// (when there are no active streams), based on the configured
// EnforcementPolicy.
func (s) TestKeepaliveServerEnforcementWithAbusiveClientNoRPC(t *testing.T) {
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: time.Second,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 20 * time.Millisecond,
Timeout: 100 * time.Millisecond,
PermitWithoutStream: true,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
if err := waitForGoAwayTooManyPings(client); err != nil {
t.Fatal(err)
}
}
// TestKeepaliveServerEnforcementWithAbusiveClientWithRPC verifies that the
// server closes a client transport when it sends too many keepalive pings
// (even when there is an active stream), based on the configured
// EnforcementPolicy.
func (s) TestKeepaliveServerEnforcementWithAbusiveClientWithRPC(t *testing.T) {
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: time.Second,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 50 * time.Millisecond,
Timeout: 100 * time.Millisecond,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
if err := waitForGoAwayTooManyPings(client); err != nil {
t.Fatal(err)
}
}
// TestKeepaliveServerEnforcementWithObeyingClientNoRPC verifies that the
// server does not close a client transport (with no active streams) which
// sends keepalive pings in accordance to the configured keepalive
// EnforcementPolicy.
func (s) TestKeepaliveServerEnforcementWithObeyingClientNoRPC(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: 40 * time.Millisecond,
PermitWithoutStream: true,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 50 * time.Millisecond,
Timeout: time.Second,
PermitWithoutStream: true,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
// Sleep for client to send ~10 keepalive pings.
time.Sleep(500 * time.Millisecond)
// Verify that the server does not close the client transport.
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
// TestKeepaliveServerEnforcementWithObeyingClientWithRPC verifies that the
// server does not close a client transport (with active streams) which
// sends keepalive pings in accordance to the configured keepalive
// EnforcementPolicy.
func (s) TestKeepaliveServerEnforcementWithObeyingClientWithRPC(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: 40 * time.Millisecond,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 50 * time.Millisecond,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
// Give keepalive enough time.
time.Sleep(500 * time.Millisecond)
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
// TestKeepaliveServerEnforcementWithDormantKeepaliveOnClient verifies that the
// server does not closes a client transport, which has been configured to send
// more pings than allowed by the server's EnforcementPolicy. This client
// transport does not have any active streams and `PermitWithoutStream` is set
// to false. This should ensure that the keepalive functionality on the client
// side enters a dormant state.
func (s) TestKeepaliveServerEnforcementWithDormantKeepaliveOnClient(t *testing.T) {
serverConfig := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepalivePolicy: keepalive.EnforcementPolicy{
MinTime: 100 * time.Millisecond,
},
}
clientOptions := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: 10 * time.Millisecond,
Timeout: 10 * time.Millisecond,
},
}
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
// No active streams on the client. Give keepalive enough time.
time.Sleep(500 * time.Millisecond)
if err := checkForHealthyStream(client); err != nil {
t.Fatalf("Stream creation failed: %v", err)
}
}
// TestTCPUserTimeout tests that the TCP_USER_TIMEOUT socket option is set to
// the keepalive timeout, as detailed in proposal A18.
func (s) TestTCPUserTimeout(t *testing.T) {
tests := []struct {
tls bool
time time.Duration
timeout time.Duration
clientWantTimeout time.Duration
serverWantTimeout time.Duration
}{
{
false,
10 * time.Second,
10 * time.Second,
10 * 1000 * time.Millisecond,
10 * 1000 * time.Millisecond,
},
{
false,
0,
0,
0,
20 * 1000 * time.Millisecond,
},
{
false,
infinity,
infinity,
0,
0,
},
{
true,
10 * time.Second,
10 * time.Second,
10 * 1000 * time.Millisecond,
10 * 1000 * time.Millisecond,
},
{
true,
0,
0,
0,
20 * 1000 * time.Millisecond,
},
{
true,
infinity,
infinity,
0,
0,
},
}
for _, tt := range tests {
sopts := &ServerConfig{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ServerParameters{
Time: tt.time,
Timeout: tt.timeout,
},
}
copts := ConnectOptions{
BufferPool: mem.DefaultBufferPool(),
KeepaliveParams: keepalive.ClientParameters{
Time: tt.time,
Timeout: tt.timeout,
},
}
if tt.tls {
copts.TransportCredentials = makeTLSCreds(t, "x509/client1_cert.pem", "x509/client1_key.pem", "x509/server_ca_cert.pem")
sopts.Credentials = makeTLSCreds(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
}
server, client, cancel := setUpWithOptions(
t,
0,
sopts,
normal,
copts,
)
defer func() {
client.Close(fmt.Errorf("closed manually by test"))
server.stop()
cancel()
}()
var sc *http2Server
var srawConn net.Conn
// Wait until the server transport is setup.
for {
server.mu.Lock()
if len(server.conns) == 0 {
server.mu.Unlock()
time.Sleep(time.Millisecond)
continue
}
for k := range server.conns {
var ok bool
sc, ok = k.(*http2Server)
if !ok {
t.Fatalf("Failed to convert %v to *http2Server", k)
}
srawConn = server.conns[k]
}
server.mu.Unlock()
break
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
if err != nil {
t.Fatalf("client.NewStream() failed: %v", err)
}
stream.Close(io.EOF)
// check client TCP user timeout only when non TLS
// TODO : find a way to get the underlying conn for client when TLS
if !tt.tls {
cltOpt, err := syscall.GetTCPUserTimeout(client.conn)
if err != nil {
t.Fatalf("syscall.GetTCPUserTimeout() failed: %v", err)
}
if cltOpt < 0 {
t.Skipf("skipping test on unsupported environment")
}
if gotTimeout := time.Duration(cltOpt) * time.Millisecond; gotTimeout != tt.clientWantTimeout {
t.Fatalf("syscall.GetTCPUserTimeout() = %d, want %d", gotTimeout, tt.clientWantTimeout)
}
}
scConn := sc.conn
if tt.tls {
if _, ok := sc.conn.(*net.TCPConn); ok {
t.Fatalf("sc.conn is should have wrapped conn with TLS")
}
scConn = srawConn
}
// verify the type of scConn (on which TCP user timeout will be got)
if _, ok := scConn.(*net.TCPConn); !ok {
t.Fatalf("server underlying conn is of type %T, want net.TCPConn", scConn)
}
srvOpt, err := syscall.GetTCPUserTimeout(scConn)
if err != nil {
t.Fatalf("syscall.GetTCPUserTimeout() failed: %v", err)
}
if gotTimeout := time.Duration(srvOpt) * time.Millisecond; gotTimeout != tt.serverWantTimeout {
t.Fatalf("syscall.GetTCPUserTimeout() = %d, want %d", gotTimeout, tt.serverWantTimeout)
}
}
}
func makeTLSCreds(t *testing.T, certPath, keyPath, rootsPath string) credentials.TransportCredentials {
cert, err := tls.LoadX509KeyPair(testdata.Path(certPath), testdata.Path(keyPath))
if err != nil {
t.Fatalf("tls.LoadX509KeyPair(%q, %q) failed: %v", certPath, keyPath, err)
}
b, err := os.ReadFile(testdata.Path(rootsPath))
if err != nil {
t.Fatalf("os.ReadFile(%q) failed: %v", rootsPath, err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(b) {
t.Fatal("failed to append certificates")
}
return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: roots,
InsecureSkipVerify: true,
})
}
// checkForHealthyStream attempts to create a stream and return error if any.
// The stream created is closed right after to avoid any leakages.
func checkForHealthyStream(client *http2Client) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
if err != nil {
return err
}
stream.Close(nil)
return nil
}
func pollForStreamCreationError(client *http2Client) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for {
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
break
}
time.Sleep(50 * time.Millisecond)
}
if ctx.Err() != nil {
return fmt.Errorf("test timed out before stream creation returned an error")
}
return nil
}
// waitForGoAwayTooManyPings waits for client to receive a GoAwayTooManyPings
// from server. It also asserts that stream creation fails after receiving a
// GoAway.
func waitForGoAwayTooManyPings(client *http2Client) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
select {
case <-client.GoAway():
if reason, _ := client.GetGoAwayReason(); reason != GoAwayTooManyPings {
return fmt.Errorf("goAwayReason is %v, want %v", reason, GoAwayTooManyPings)
}
case <-ctx.Done():
return fmt.Errorf("test timed out before getting GoAway with reason:GoAwayTooManyPings from server")
}
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err == nil {
return fmt.Errorf("stream creation succeeded after receiving a GoAway from the server")
}
return nil
}

View File

@@ -0,0 +1,40 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"fmt"
"google.golang.org/grpc/grpclog"
internalgrpclog "google.golang.org/grpc/internal/grpclog"
)
var logger = grpclog.Component("transport")
func prefixLoggerForServerTransport(p *http2Server) *internalgrpclog.PrefixLogger {
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[server-transport %p] ", p))
}
func prefixLoggerForServerHandlerTransport(p *serverHandlerTransport) *internalgrpclog.PrefixLogger {
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[server-handler-transport %p] ", p))
}
func prefixLoggerForClientTransport(p *http2Client) *internalgrpclog.PrefixLogger {
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[client-transport %p] ", p))
}

View File

@@ -0,0 +1,46 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package networktype declares the network type to be used in the default
// dialer. Attribute of a resolver.Address.
package networktype
import (
"google.golang.org/grpc/resolver"
)
// keyType is the key to use for storing State in Attributes.
type keyType string
const key = keyType("grpc.internal.transport.networktype")
// Set returns a copy of the provided address with attributes containing networkType.
func Set(address resolver.Address, networkType string) resolver.Address {
address.Attributes = address.Attributes.WithValue(key, networkType)
return address
}
// Get returns the network type in the resolver.Address and true, or "", false
// if not present.
func Get(address resolver.Address) (string, bool) {
v := address.Attributes.Value(key)
if v == nil {
return "", false
}
return v.(string), true
}

116
internal/transport/proxy.go Normal file
View File

@@ -0,0 +1,116 @@
/*
*
* Copyright 2017 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"bufio"
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/proxyattributes"
"google.golang.org/grpc/resolver"
)
const proxyAuthHeaderKey = "Proxy-Authorization"
// To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader.
// It's possible that this reader reads more than what's need for the response
// and stores those bytes in the buffer. bufConn wraps the original net.Conn
// and the bufio.Reader to make sure we don't lose the bytes in the buffer.
type bufConn struct {
net.Conn
r io.Reader
}
func (c *bufConn) Read(b []byte) (int, error) {
return c.r.Read(b)
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) {
defer func() {
if err != nil {
conn.Close()
}
}()
req := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{Host: opts.ConnectAddr},
Header: map[string][]string{"User-Agent": {grpcUA}},
}
if user := opts.User; user != nil {
u := user.Username()
p, _ := user.Password()
req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))
}
if err := sendHTTPRequest(ctx, req, conn); err != nil {
return nil, fmt.Errorf("failed to write the HTTP request: %v", err)
}
r := bufio.NewReader(conn)
resp, err := http.ReadResponse(r, req)
if err != nil {
return nil, fmt.Errorf("reading server HTTP response: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
dump, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status)
}
return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump)
}
// The buffer could contain extra bytes from the target server, so we can't
// discard it. However, in many cases where the server waits for the client
// to send the first message (e.g. when TLS is being used), the buffer will
// be empty, so we can avoid the overhead of reading through this buffer.
if r.Buffered() != 0 {
return &bufConn{Conn: conn, r: r}, nil
}
return conn, nil
}
// proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake.
func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) {
conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr)
if err != nil {
return nil, err
}
return doHTTPConnectHandshake(ctx, conn, grpcUA, opts)
}
func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error {
req = req.WithContext(ctx)
if err := req.Write(conn); err != nil {
return fmt.Errorf("failed to write the HTTP request: %v", err)
}
return nil
}

View File

@@ -0,0 +1,441 @@
/*
*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport_test
import (
"context"
"encoding/base64"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"testing"
"time"
"golang.org/x/net/http/httpproxy"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/resolver/delegatingresolver"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/proxyserver"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
)
const defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func startBackendServer(t *testing.T) *stubserver.StubServer {
t.Helper()
backend := &stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) { return &testpb.Empty{}, nil },
}
if err := backend.StartServer(); err != nil {
t.Fatalf("failed to start backend: %v", err)
}
t.Logf("Started TestService backend at: %q", backend.Address)
t.Cleanup(backend.Stop)
return backend
}
func isIPAddr(addr string) bool {
_, err := netip.ParseAddr(addr)
return err == nil
}
func overrideTestHTTPSProxy(t *testing.T, proxyAddr string) {
t.Helper()
hpfe := func(*http.Request) (*url.URL, error) {
return &url.URL{
Scheme: "https",
Host: proxyAddr,
}, nil
}
originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
delegatingresolver.HTTPSProxyFromEnvironment = hpfe
t.Cleanup(func() { delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe })
}
// Tests the scenario where grpc.Dial is performed using a proxy with the
// default resolver in the target URI. The test verifies that the connection is
// established to the proxy server, sends the unresolved target URI in the HTTP
// CONNECT request and is successfully connected to the backend server.
func (s) TestGRPCDialWithProxy(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
if got, want := host, "localhost"; got != want {
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
}
}
pServer := proxyserver.New(t, reqCheck, false)
// Use "localhost:<port>" to verify the proxy address is handled
// correctly by the delegating resolver and connects to the proxy server
// correctly even when unresolved.
pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pServer.Addr))
overrideTestHTTPSProxy(t, pAddr)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall failed: %v", err)
}
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}
// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns"
// scheme for the target. The test verifies that the proxy URI is correctly
// resolved and that the target URI resolution on the client preserves the
// original behavior of `grpc.Dial`. It also ensures that a connection is
// established to the proxy server, with the resolved target URI sent in the
// HTTP CONNECT request, successfully connecting to the backend server.
func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
if got, want := isIPAddr(host), true; got != want {
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
}
}
pServer := proxyserver.New(t, reqCheck, false)
overrideTestHTTPSProxy(t, pServer.Addr)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.Dial("dns:///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.Dial(%s) failed: %v", "dns:///"+unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall failed: %v", err)
}
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}
// Tests the scenario where `grpc.NewClient` is used with the default DNS
// resolver for the target URI and a proxy is configured. The test verifies
// that the client resolves proxy URI, connects to the proxy server, sends the
// unresolved target URI in the HTTP CONNECT request, and successfully
// establishes a connection to the backend server.
func (s) TestNewClientWithProxy(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
if got, want := host, "localhost"; got != want {
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
}
}
pServer := proxyserver.New(t, reqCheck, false)
// Use "localhost:<port>" to verify the proxy address is handled
// correctly by the delegating resolver and connects to the proxy server
// correctly even when unresolved.
pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pServer.Addr))
overrideTestHTTPSProxy(t, pAddr)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall failed: %v", err)
}
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}
// Tests the scenario where grpc.NewClient is used with a custom target URI
// scheme and a proxy is configured. The test verifies that the client
// successfully connects to the proxy server, resolves the proxy URI correctly,
// includes the resolved target URI in the HTTP CONNECT request, and
// establishes a connection to the backend server.
func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
if got, want := isIPAddr(host), true; got != want {
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
}
}
pServer := proxyserver.New(t, reqCheck, false)
overrideTestHTTPSProxy(t, pServer.Addr)
// Create and update a custom resolver for target URI.
targetResolver := manual.NewBuilderWithScheme("test")
resolver.Register(targetResolver)
targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backend.Address}}}}})
// Dial to the proxy server.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}
// Tests the scenario where grpc.NewClient is used with the default "dns"
// resolver and the dial option grpc.WithLocalDNSResolution() is set,
// enabling target resolution on the client. The test verifies that target
// resolution happens on the client by sending resolved target URI in HTTP
// CONNECT request, the proxy URI is resolved correctly, and the connection is
// successfully established with the backend server through the proxy.
func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
if got, want := isIPAddr(host), true; got != want {
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
}
}
pServer := proxyserver.New(t, reqCheck, false)
overrideTestHTTPSProxy(t, pServer.Addr)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithLocalDNSResolution(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall failed: %v", err)
}
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}
// Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set,
// explicitly disabling proxy usage. The test verifies that the client does not
// dial the proxy but directly connects to the backend server. It also checks
// that the proxy resolution function is not called and that the proxy server
// never receives a connection request.
func (s) TestNewClientWithNoProxy(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") }
pServer := proxyserver.New(t, reqCheck, false)
overrideTestHTTPSProxy(t, pServer.Addr)
dopts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithNoProxy(), // Disable proxy.
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(unresolvedTargetURI, dopts...)
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
// Create a test service client and make an RPC call.
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer()
// set. The test verifies that the client bypasses proxy dialing and uses the
// custom dialer instead. It ensures that the proxy server is never dialed, the
// proxy resolution function is not triggered, and the custom dialer is invoked
// as expected.
func (s) TestNewClientWithContextDialer(t *testing.T) {
backend := startBackendServer(t)
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") }
pServer := proxyserver.New(t, reqCheck, false)
overrideTestHTTPSProxy(t, pServer.Addr)
// Create a custom dialer that directly dials the backend.
customDialer := func(_ context.Context, unresolvedTargetURI string) (net.Conn, error) {
return net.Dial("tcp", unresolvedTargetURI)
}
dopts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(customDialer), // Use a custom dialer.
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(unresolvedTargetURI, dopts...)
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
client := testgrpc.NewTestServiceClient(conn)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// Tests the scenario where grpc.NewClient is used with the default DNS resolver
// for targetURI and a proxy. The test verifies that the client connects to the
// proxy server, sends the unresolved target URI in the HTTP CONNECT request,
// and successfully connects to the backend. Additionally, it checks that the
// correct user information is included in the Proxy-Authorization header of
// the CONNECT request. The test also ensures that target resolution does not
// happen on the client.
func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) {
unresolvedTargetURI := "example.test"
const (
user = "notAUser"
password = "notAPassword"
)
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
if got, want := req.URL.Host, "example.test:443"; got != want {
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
}
wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr {
gotDecoded, err := base64.StdEncoding.DecodeString(got)
if err != nil {
t.Errorf("failed to decode Proxy-Authorization header: %v", err)
}
wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr)
t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded)
}
}
pServer := proxyserver.New(t, reqCheck, false)
t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr)
// Use the httpproxy package functions instead of `http.ProxyFromEnvironment`
// because the latter reads proxy-related environment variables only once at
// initialization. This behavior causes issues when running test multiple
// times, as changes to environment variables during tests would be ignored.
// By using `httpproxy.FromEnvironment()`, we ensure proxy settings are read dynamically.
origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment
delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
}
defer func() {
delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
}
defer conn.Close()
// Send an empty RPC to the backend through the proxy.
client := testgrpc.NewTestServiceClient(conn)
client.EmptyCall(ctx, &testpb.Empty{})
if !proxyCalled {
t.Fatalf("Proxy not connected")
}
}

View File

@@ -0,0 +1,105 @@
//go:build !race
// +build !race
/*
*
* Copyright 2017 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"context"
"net"
"net/http"
"net/netip"
"testing"
"time"
"google.golang.org/grpc/internal/proxyattributes"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/proxyserver"
"google.golang.org/grpc/resolver"
)
func (s) TestHTTPConnectWithServerHello(t *testing.T) {
serverMessage := []byte("server-hello")
blis, err := testutils.LocalTCPListener()
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
reqCheck := func(req *http.Request) {
if req.Method != http.MethodConnect {
t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect)
}
host, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
t.Error(err)
}
_, err = netip.ParseAddr(host)
if err != nil {
t.Error(err)
}
}
pServer := proxyserver.New(t, reqCheck, true)
msg := []byte{4, 3, 5, 2}
recvBuf := make([]byte, len(msg))
done := make(chan error, 1)
go func() {
in, err := blis.Accept()
if err != nil {
done <- err
return
}
defer in.Close()
in.Write(serverMessage)
in.Read(recvBuf)
done <- nil
}()
// Dial to proxy server.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c, err := proxyDial(ctx, resolver.Address{Addr: pServer.Addr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()})
if err != nil {
t.Fatalf("HTTP connect Dial failed: %v", err)
}
defer c.Close()
c.SetDeadline(time.Now().Add(defaultTestTimeout))
// Send msg on the connection.
c.Write(msg)
if err := <-done; err != nil {
t.Fatalf("Failed to accept: %v", err)
}
// Check received msg.
if string(recvBuf) != string(msg) {
t.Fatalf("Received msg: %v, want %v", recvBuf, msg)
}
if len(serverMessage) > 0 {
gotServerMessage := make([]byte, len(serverMessage))
if _, err := c.Read(gotServerMessage); err != nil {
t.Errorf("Got error while reading message from server: %v", err)
return
}
if string(gotServerMessage) != string(serverMessage) {
t.Errorf("Message from server: %v, want %v", gotServerMessage, serverMessage)
}
}
}

View File

@@ -0,0 +1,39 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package readyreader
import "syscall"
func isRawConnSupported() bool {
return true
}
// sysRead uses the standard syscall package rather than the modern unix package
// to avoid triggering the race detector. Because both packages perform sync
// operations on a local variable to satisfy the race detector, mixing them
// for read and write syscalls causes data races. We use syscall here to remain
// consistent with net.Conn implementations in standard library.
func sysRead(fd uintptr, p []byte) (int, error) {
return syscall.Read(int(fd), p)
}
// wouldBlock checks standard Unix non-blocking errors.
func wouldBlock(err error) bool {
return err == syscall.EAGAIN || err == syscall.EWOULDBLOCK
}

View File

@@ -0,0 +1,35 @@
//go:build !linux
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package readyreader
func isRawConnSupported() bool {
return false
}
// sysRead is not implemented. Support can be added in the future if necessary.
func sysRead(uintptr, []byte) (int, error) {
panic("RawConn functionality is not implemented for non-unix platforms.")
}
// wouldBlock is not implemented. Support can be added in the future if necessary.
func wouldBlock(error) bool {
panic("RawConn functionality is not implemented for non-unix platforms.")
}

View File

@@ -0,0 +1,253 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package readyreader provides utilities to perform non-memory-pinning reads.
package readyreader
import (
"io"
"net"
"syscall"
"google.golang.org/grpc/mem"
)
// Reader is an optional interface that can be implemented by [net.Conn]
// implementations to enable gRPC to perform non-memory-pinning reads.
type Reader interface {
// ReadOnReady waits for data to arrive, fetches a buffer, and performs a
// read. When the underlying IO is readable, it allocates a buffer of size
// bufSize from the pool and reads up to bufSize bytes into the buffer.
//
// It returns a pointer to the buffer so it can be returned to the pool
// later, the number of bytes read, and an error.
//
// Callers should always process the n > 0 bytes returned before considering
// the error. Doing so correctly handles I/O errors that happen after
// reading some bytes, as well as both of the allowed EOF behaviors.
ReadOnReady(bufSize int, pool mem.BufferPool) (b *[]byte, n int, err error)
}
// nonBlockingReader is optimized for non-memory-pinning reads using the RawConn
// interface.
type nonBlockingReader struct {
raw syscall.RawConn
// The following fields are stored as field to avoid heap allocations.
state readState
doRead func(fd uintptr) bool
}
type readState struct {
// Request params.
bufSize int
pool mem.BufferPool
// Response params.
readError error
bytesRead int
buf *[]byte
}
// NewNonBlocking returns a ReadyReader if the passed reader supports
// non-memory-pinning reads, else nil.
func NewNonBlocking(r io.Reader) Reader {
if rr, ok := r.(Reader); ok {
return rr
}
if !isRawConnSupported() {
return nil
}
// We restrict the types before asserting syscall.Conn. The credentials
// package may return a wrapper that implements syscall.Conn by embedding
// both the raw connection and the encrypted connection. If the code
// attempts to read directly from the raw syscall.RawConn, it would read
// encrypted data.
switch r.(type) {
case *net.TCPConn, *net.UDPConn, *net.UnixConn, *net.IPConn:
default:
return nil
}
sysConn, ok := r.(syscall.Conn)
if !ok {
return nil
}
raw, err := sysConn.SyscallConn()
if err != nil {
return nil
}
rr := &nonBlockingReader{raw: raw}
rr.doRead = func(fd uintptr) bool {
s := &rr.state
s.buf = s.pool.Get(s.bufSize)
s.bytesRead, s.readError = sysRead(fd, *s.buf)
if s.readError != nil {
s.pool.Put(s.buf)
s.buf = nil
}
return !wouldBlock(s.readError)
}
return rr
}
func (c *nonBlockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
c.state = readState{
pool: pool,
bufSize: bufSize,
}
err := c.raw.Read(c.doRead)
buf := c.state.buf
n := c.state.bytesRead
readErr := c.state.readError
c.state = readState{}
if err != nil {
if buf != nil {
pool.Put(buf)
}
return nil, 0, err
}
if readErr != nil {
// buffer is already released in the callback.
return nil, 0, readErr
}
if n == 0 {
// syscall.Read doesn't consider a graceful socket closure to be an
// error condition, but Go's io.Reader expects an EOF error.
pool.Put(buf)
return nil, 0, io.EOF
}
return buf, n, nil
}
type blockingReader struct {
reader io.Reader
}
func (c *blockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
buf := pool.Get(bufSize)
n, err := c.reader.Read(*buf)
if err != nil {
pool.Put(buf)
return nil, 0, err
}
return buf, n, nil
}
// New detects if [syscall.RawConn] is available for non-memory-pinning reads.
// If [syscall.RawConn] is unavailable, it falls back to using the simpler
// [io.Reader] interface for reads.
func New(r io.Reader) Reader {
if r := NewNonBlocking(r); r != nil {
return r
}
return &blockingReader{reader: r}
}
// bufReadyReader implements buffering for a ReadyReader object.
// A new bufReadyReader is created by calling [NewBuffered].
type bufReadyReader struct {
buf *[]byte
pool mem.BufferPool
bufSize int
rd Reader // reader provided by the caller
r, w int // buf read and write positions
err error
constPool constBufferPool // stored as a field to avoid heap allocations.
}
// NewBuffered returns a new [io.Reader] with a buffer of the specified size
// which is allocated from the provided pool.
func NewBuffered(rd Reader, size int, pool mem.BufferPool) io.Reader {
return &bufReadyReader{
rd: rd,
pool: pool,
bufSize: size,
}
}
func (b *bufReadyReader) readErr() error {
err := b.err
b.err = nil
return err
}
func (b *bufReadyReader) buffered() int { return b.w - b.r }
// Read reads data into p. It returns the number of bytes read into p. The
// bytes are taken from at most one Read on the underlying [ReadyReader],
// hence n may be less than len(p). If the underlying [ReadyReader] can return
// a non-zero count with io.EOF, then this Read method can do so as well; see
// the [io.Reader] docs.
func (b *bufReadyReader) Read(p []byte) (n int, err error) {
n = len(p)
if n == 0 {
if b.buffered() > 0 {
return 0, nil
}
return 0, b.readErr()
}
if b.r == b.w {
if b.err != nil {
return 0, b.readErr()
}
if len(p) >= b.bufSize {
// Large read, empty buffer.
// Read directly into p to avoid copy.
b.constPool.buffer = p
_, n, b.err = b.rd.ReadOnReady(len(p), &b.constPool)
return n, b.readErr()
}
// One read.
b.r = 0
b.w = 0
b.buf, n, b.err = b.rd.ReadOnReady(b.bufSize, b.pool)
if n == 0 {
if b.buf != nil {
b.pool.Put(b.buf)
b.buf = nil
}
return 0, b.readErr()
}
b.w += n
}
// copy as much as we can
// b.buf must be non-nil since b.r != b.w.
buf := *b.buf
n = copy(p, buf[b.r:b.w])
b.r += n
if b.r == b.w {
// Consumed entire buffer, release it.
b.pool.Put(b.buf)
b.buf = nil
}
return n, nil
}
type constBufferPool struct {
buffer []byte
}
func (p *constBufferPool) Get(int) *[]byte {
return &p.buffer
}
func (p *constBufferPool) Put(*[]byte) {}

View File

@@ -0,0 +1,414 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package readyreader_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"runtime"
"strings"
"testing"
"time"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/transport/readyreader"
"google.golang.org/grpc/mem"
)
var (
defaultTestTimeout = 10 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// TestReadyReader_NonRawConn verifies that ReadOnReady correctly reads data
// from a net.Conn that doesn't support non-memory-pinning reads.
func (s) TestReadyReader_NonRawConn(t *testing.T) {
data := []byte("hello world")
reader, writer := net.Pipe()
go writer.Write(data)
pool := mem.DefaultBufferPool()
readyReader := readyreader.New(reader)
bufHandle, n, err := readyReader.ReadOnReady(1024, pool)
if err != nil {
t.Fatalf("ReadOnReady() failed: %v", err)
}
defer pool.Put(bufHandle)
if n != len(data) {
t.Errorf("n = %d; want %d", n, len(data))
}
if !bytes.Equal((*bufHandle)[:n], data) {
t.Errorf("Read data = %s; want %s", string((*bufHandle)[:n]), string(data))
}
}
func (s) TestReadyReader_EOF(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("net.Listen failed: %v", err)
}
defer ln.Close()
data := []byte("hello")
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
if _, err := conn.Write(data); err != nil {
t.Errorf("Failed to write data: %v", err)
return
}
conn.Close()
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("net.Dial failed: %v", err)
}
defer conn.Close()
pool := mem.DefaultBufferPool()
rr := readyreader.New(conn)
res, _, err := rr.ReadOnReady(len(data), pool)
if err != nil {
t.Errorf("Failed to read: %v", err)
return
}
if !bytes.Equal(*res, data) {
t.Errorf("Read data = %s; want %s", string(*res), string(data))
}
pool.Put(res)
// Since the server closes the TCP connection, the next read should return
// an io.EOF.
res, _, err = rr.ReadOnReady(len(data), pool)
if err != io.EOF {
t.Errorf("Read after server connection close returned err = %v; want io.EOF", err)
}
if res != nil {
t.Error("ReadOnReady() returned non-nil buffer.")
}
}
// Tests the behavior of readers wrapping TCP connections. It ensures that read
// operations do not hold onto an allocated buffer while the connection is idle.
// Buffers should only be permanently allocated from the pool when data is
// actually ready on the socket, optimizing memory usage for idle connections.
func (s) TestReadyReader_TCP(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("This test is only applicable for Linux, as RawConn functionality is not implemented for non-linux platforms.")
}
tests := []struct {
name string
read func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error)
}{
{
name: "ReadyReader",
read: func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error) {
rr := readyreader.New(conn)
bufHandle, n, err := rr.ReadOnReady(readBufSize, pool)
if err != nil {
return nil, err
}
defer pool.Put(bufHandle)
res := make([]byte, n)
copy(res, (*bufHandle)[:n])
return res, nil
},
},
{
name: "BufReadyReader",
read: func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error) {
rr := readyreader.New(conn)
bufRR := readyreader.NewBuffered(rr, readBufSize, pool)
buf := make([]byte, 100)
n, err := bufRR.Read(buf)
if err != nil {
return nil, err
}
return buf[:n], nil
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout)
defer cancel()
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("net.Listen failed: %v", err)
}
defer ln.Close()
data := []byte("hello tcp delayed")
connCh := make(chan net.Conn)
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
connCh <- conn
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("net.Dial failed: %v", err)
}
defer conn.Close()
serverConn := <-connCh
defer serverConn.Close()
pool := newTrackingPool(mem.DefaultBufferPool())
const readBufSize = 1024
resCh := make(chan []byte)
go func() {
res, err := tc.read(conn, pool, readBufSize)
if err != nil {
t.Errorf("Failed to read: %v", err)
return
}
resCh <- res
}()
// Verify that no read buffer is allocated for a short while. If it
// is allocated (e.g. for a probe), it must be returned immediately.
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
if n, err := pool.requestChan.Receive(sCtx); err == nil {
if n != readBufSize {
t.Fatalf("Unexpected request buffer size: got %d, want %d", n, readBufSize)
}
if n, err := pool.putChan.Receive(ctx); err != nil {
t.Fatal("Read buffer allocated and NOT returned while idle.")
} else if n != readBufSize {
t.Fatalf("Unexpected returned buffer size: got %d, want %d", n, readBufSize)
}
}
// Write data to unblock.
serverConn.Write(data)
if n, err := pool.requestChan.Receive(ctx); err != nil {
t.Fatal("Context timed out while waiting for a read buffer to be allocated.")
} else if n != readBufSize {
t.Fatalf("Unexpected request buffer size: got %d, want %d", n, readBufSize)
}
var res []byte
select {
case res = <-resCh:
case <-ctx.Done():
t.Fatal("Context timed out waiting for read to complete.")
}
if !bytes.Equal(res, data) {
t.Errorf("Read data = %s; want %s", string(res), string(data))
}
})
}
}
// trackingBufferPool wraps a mem.BufferPool and provides channels to track
// when buffers are requested and returned, useful for verifying allocation
// behavior in tests.
type trackingBufferPool struct {
mem.BufferPool
requestChan testutils.Channel
putChan testutils.Channel
}
func newTrackingPool(pool mem.BufferPool) *trackingBufferPool {
return &trackingBufferPool{
BufferPool: pool,
requestChan: *testutils.NewChannelWithSize(1),
putChan: *testutils.NewChannelWithSize(1),
}
}
func (p *trackingBufferPool) Get(size int) *[]byte {
p.requestChan.Replace(size)
return p.BufferPool.Get(size)
}
func (p *trackingBufferPool) Put(b *[]byte) {
p.putChan.Replace(len(*b))
p.BufferPool.Put(b)
}
// Call Read to accumulate the text of a file
func reads(buf io.Reader, m int) string {
var b [1000]byte
nb := 0
for {
n, err := buf.Read(b[nb : nb+m])
nb += n
if err == io.EOF {
break
}
}
return string(b[0:nb])
}
type bufReader struct {
name string
fn func(io.Reader) string
}
var bufreaders = []bufReader{
{"1", func(b io.Reader) string { return reads(b, 1) }},
{"2", func(b io.Reader) string { return reads(b, 2) }},
{"3", func(b io.Reader) string { return reads(b, 3) }},
{"4", func(b io.Reader) string { return reads(b, 4) }},
{"5", func(b io.Reader) string { return reads(b, 5) }},
{"7", func(b io.Reader) string { return reads(b, 7) }},
}
const minReadBufferSize = 16
var bufsizes = []int{
0, minReadBufferSize, 23, 32, 46, 64, 93, 128, 1024, 4096,
}
func (s) TestBufReader(t *testing.T) {
var texts [31]string
str := ""
all := ""
for i := range len(texts) - 1 {
texts[i] = str + "\n"
all += texts[i]
str += string(rune(i%26 + 'a'))
}
texts[len(texts)-1] = all
for _, text := range texts {
for _, bufreader := range bufreaders {
for _, bufsize := range bufsizes {
// We don't use t.Run() here to avoid excessive logging due to
// the large number of subtests.
read := readyreader.New(strings.NewReader(text))
buf := readyreader.NewBuffered(read, bufsize, mem.DefaultBufferPool())
s := bufreader.fn(buf)
if s != text {
t.Errorf("fn=%s bufsize=%d want=%q got=%q", bufreader.name, bufsize, text, s)
}
}
}
}
}
func (s) TestBufReader_ReadEmptyBuffer(t *testing.T) {
rr := readyreader.New(new(bytes.Buffer))
l := readyreader.NewBuffered(rr, minReadBufferSize, mem.DefaultBufferPool())
b := make([]byte, 100)
n, err := l.Read(b)
if err != io.EOF {
t.Errorf("expected EOF from Read, got %q %v", b[:n], err)
}
}
type errorThenGoodReader struct {
didErr bool
nread int
}
var errFake = errors.New("fake error")
func (r *errorThenGoodReader) Read(p []byte) (int, error) {
r.nread++
if !r.didErr {
r.didErr = true
return 0, errFake
}
return len(p), nil
}
func (s) TestBufReader_ClearError(t *testing.T) {
r := &errorThenGoodReader{}
b := readyreader.NewBuffered(readyreader.New(r), minReadBufferSize, mem.DefaultBufferPool())
buf := make([]byte, 1)
if _, err := b.Read(nil); err != nil {
t.Fatalf("1st nil Read = %v; want nil", err)
}
if _, err := b.Read(buf); err != errFake {
t.Fatalf("1st Read = %v; want errFake", err)
}
if _, err := b.Read(nil); err != nil {
t.Fatalf("2nd nil Read = %v; want nil", err)
}
if _, err := b.Read(buf); err != nil {
t.Fatalf("3rd Read with buffer = %v; want nil", err)
}
if r.nread != 2 {
t.Errorf("num reads = %d; want 2", r.nread)
}
}
type emptyThenNonEmptyReader struct {
r io.Reader
n int
}
func (r *emptyThenNonEmptyReader) Read(p []byte) (int, error) {
if r.n <= 0 {
return r.r.Read(p)
}
r.n--
return 0, nil
}
func (s) TestBufReader_ReadZero(t *testing.T) {
for _, size := range []int{100, 2} {
t.Run(fmt.Sprintf("bufsize=%d", size), func(t *testing.T) {
r := io.MultiReader(strings.NewReader("abc"), &emptyThenNonEmptyReader{r: strings.NewReader("def"), n: 1})
br := readyreader.NewBuffered(readyreader.New(r), size, mem.DefaultBufferPool())
want := func(s string, wantErr error) {
p := make([]byte, 50)
n, err := br.Read(p)
if err != wantErr || n != len(s) || string(p[:n]) != s {
t.Fatalf("read(%d) = %q, %v, want %q, %v", len(p), string(p[:n]), err, s, wantErr)
}
t.Logf("read(%d) = %q, %v", len(p), string(p[:n]), err)
}
want("abc", nil)
want("", nil)
want("def", nil)
want("", io.EOF)
})
}
}

View File

@@ -0,0 +1,185 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport_test
import (
"context"
"fmt"
"io"
"sync/atomic"
"testing"
"time"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/channelz"
"google.golang.org/grpc/internal/stubserver"
transportinternal "google.golang.org/grpc/internal/transport/internal"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
func overrideTimeNowFunc(t *testing.T, f func() int64) {
orig := transportinternal.TimeNowFunc
transportinternal.TimeNowFunc = f
t.Cleanup(func() { transportinternal.TimeNowFunc = orig })
}
func verifyResultWithDelay(ctx context.Context, f func() (bool, error)) error {
var lastErr error
for ; ctx.Err() == nil; <-time.After(10 * time.Millisecond) {
var ok bool
if ok, lastErr = f(); ok {
return nil
}
}
return fmt.Errorf("Timeout when waiting for channelz to contain expected data. Last seen error: %v", lastErr)
}
func (s) TestChannelz_ServerSocketMetricsLastMessageTimestamps(t *testing.T) {
channelz.TurnOn()
defer internal.ChannelzTurnOffForTesting()
// Override timeNowFunc for the duration of the test.
var curTime atomic.Int64
overrideTimeNowFunc(t, func() int64 {
curTime.Add(1)
return curTime.Load()
})
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Start a stub server that echoes back messages sent by the client. This
// allows us to test both LastMessageReceivedTimestamp and
// LastMessageSentTimestamp on the server side.
var blockBeforeSend = make(chan struct{}, 1)
ss := &stubserver.StubServer{
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
for {
req, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
select {
case <-blockBeforeSend:
case <-ctx.Done():
return ctx.Err()
}
if err := stream.Send(&testpb.StreamingOutputCallResponse{Payload: req.GetPayload()}); err != nil {
return err
}
}
},
}
if err := ss.Start(nil); err != nil {
t.Fatalf("Failed to start test server: %v", err)
}
defer ss.Stop()
// Wait for the server to be up and registered with channelz.
var svrID int64
if err := verifyResultWithDelay(ctx, func() (bool, error) {
ssrvs, _ := channelz.GetServers(0, 0)
for _, s := range ssrvs {
for id := range s.ListenSockets() {
skt := channelz.GetSocket(id)
if skt != nil && skt.LocalAddr.String() == ss.Address {
svrID = s.ID
return true, nil
}
}
}
return false, fmt.Errorf("could not find server with address %s in channelz", ss.Address)
}); err != nil {
t.Fatal(err)
}
stream, err := ss.Client.FullDuplexCall(ctx)
if err != nil {
t.Fatalf("FullDuplexCall() failed: %v", err)
}
// Wait for the message from the client to be received on the server and
// timestamp updated.
wantTimestamp := int64(1)
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
t.Fatalf("stream.Send(_) = %v, want <nil>", err)
}
if err := verifyResultWithDelay(ctx, func() (bool, error) {
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
if len(ns) == 0 {
return false, fmt.Errorf("no server sockets found")
}
sktData := &ns[0].SocketMetrics
if sktData.MessagesReceived.Load() < 1 {
return false, fmt.Errorf("server has not received any messages yet")
}
if gotTimestamp := sktData.LastMessageReceivedTimestamp.Load(); gotTimestamp != wantTimestamp {
return false, fmt.Errorf("LastMessageReceivedTimestamp is %d, want %d", gotTimestamp, wantTimestamp)
}
return true, nil
}); err != nil {
t.Fatal(err)
}
blockBeforeSend <- struct{}{}
// Wait for the message to be sent by the server and timestamp updated.
if _, err := stream.Recv(); err != nil {
t.Fatalf("stream.Recv() = %v, want <nil>", err)
}
wantTimestamp = int64(2)
if err := verifyResultWithDelay(ctx, func() (bool, error) {
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
sktData := &ns[0].SocketMetrics
if sktData.MessagesSent.Load() == 0 {
return false, fmt.Errorf("server has not sent any messages yet")
}
if gotTimestamp := sktData.LastMessageSentTimestamp.Load(); gotTimestamp != wantTimestamp {
return false, fmt.Errorf("LastMessageSentTimestamp = %d, want = %d", gotTimestamp, wantTimestamp)
}
return true, nil
}); err != nil {
t.Fatal(err)
}
// Send another message to verify that LastMessageReceivedTimestamp is
// updated again.
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
t.Fatalf("stream.Send(_) = %v, want <nil>", err)
}
wantTimestamp = int64(3)
if err := verifyResultWithDelay(ctx, func() (bool, error) {
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
sktData := &ns[0].SocketMetrics
if sktData.MessagesReceived.Load() < 2 {
return false, fmt.Errorf("server has not received second message yet")
}
if gotTimestamp := sktData.LastMessageReceivedTimestamp.Load(); gotTimestamp != wantTimestamp {
return false, fmt.Errorf("LastMessageReceivedTimestamp is %d, want %d", gotTimestamp, wantTimestamp)
}
return true, nil
}); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,189 @@
/*
*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package transport
import (
"context"
"errors"
"strings"
"sync"
"sync/atomic"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// ServerStream implements streaming functionality for a gRPC server.
type ServerStream struct {
Stream // Embed for common stream functionality.
st internalServerTransport
ctxDone <-chan struct{} // closed at the end of stream. Cache of ctx.Done() (for performance)
// cancel is invoked at the end of stream to cancel ctx. It also stops the
// timer for monitoring the rpc deadline if configured.
cancel func()
// Holds compressor names passed in grpc-accept-encoding metadata from the
// client.
clientAdvertisedCompressors string
// hdrMu protects outgoing header and trailer metadata.
hdrMu sync.Mutex
header metadata.MD // the outgoing header metadata. Updated by WriteHeader.
headerSent atomic.Bool // atomically set when the headers are sent out.
headerWireLength int
}
// Read reads an n byte message from the input stream.
func (s *ServerStream) Read(n int) (mem.BufferSlice, error) {
b, err := s.Stream.read(n)
if err == nil {
s.st.incrMsgRecv()
}
return b, err
}
// SendHeader sends the header metadata for the given stream.
func (s *ServerStream) SendHeader(md metadata.MD) error {
return s.st.writeHeader(s, md)
}
// Write writes the hdr and data bytes to the output stream.
func (s *ServerStream) Write(hdr []byte, data mem.BufferSlice, opts *WriteOptions) error {
return s.st.write(s, hdr, data, opts)
}
// WriteStatus sends the status of a stream to the client. WriteStatus is
// the final call made on a stream and always occurs.
func (s *ServerStream) WriteStatus(st *status.Status) error {
return s.st.writeStatus(s, st)
}
// isHeaderSent indicates whether headers have been sent.
func (s *ServerStream) isHeaderSent() bool {
return s.headerSent.Load()
}
// updateHeaderSent updates headerSent and returns true
// if it was already set.
func (s *ServerStream) updateHeaderSent() bool {
return s.headerSent.Swap(true)
}
// RecvCompress returns the compression algorithm applied to the inbound
// message. It is empty string if there is no compression applied.
func (s *ServerStream) RecvCompress() string {
return s.recvCompress
}
// SendCompress returns the send compressor name.
func (s *ServerStream) SendCompress() string {
return s.sendCompress
}
// ContentSubtype returns the content-subtype for a request. For example, a
// content-subtype of "proto" will result in a content-type of
// "application/grpc+proto". This will always be lowercase. See
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
// more details.
func (s *ServerStream) ContentSubtype() string {
return s.contentSubtype
}
// SetSendCompress sets the compression algorithm to the stream.
func (s *ServerStream) SetSendCompress(name string) error {
if s.isHeaderSent() || s.getState() == streamDone {
return errors.New("transport: set send compressor called after headers sent or stream done")
}
s.sendCompress = name
return nil
}
// SetContext sets the context of the stream. This will be deleted once the
// stats handler callouts all move to gRPC layer.
func (s *ServerStream) SetContext(ctx context.Context) {
s.ctx = ctx
}
// ClientAdvertisedCompressors returns the compressor names advertised by the
// client via grpc-accept-encoding header.
func (s *ServerStream) ClientAdvertisedCompressors() []string {
values := strings.Split(s.clientAdvertisedCompressors, ",")
for i, v := range values {
values[i] = strings.TrimSpace(v)
}
return values
}
// Header returns the header metadata of the stream. It returns the out header
// after t.WriteHeader is called. It does not block and must not be called
// until after WriteHeader.
func (s *ServerStream) Header() (metadata.MD, error) {
// Return the header in stream. It will be the out
// header after t.WriteHeader is called.
return s.header.Copy(), nil
}
// HeaderWireLength returns the size of the headers of the stream as received
// from the wire.
func (s *ServerStream) HeaderWireLength() int {
return s.headerWireLength
}
// SetHeader sets the header metadata. This can be called multiple times.
// This should not be called in parallel to other data writes.
func (s *ServerStream) SetHeader(md metadata.MD) error {
if md.Len() == 0 {
return nil
}
if s.isHeaderSent() || s.getState() == streamDone {
return ErrIllegalHeaderWrite
}
s.hdrMu.Lock()
s.header = metadata.Join(s.header, md)
s.hdrMu.Unlock()
return nil
}
// SetTrailer sets the trailer metadata which will be sent with the RPC status
// by the server. This can be called multiple times.
// This should not be called parallel to other data writes.
func (s *ServerStream) SetTrailer(md metadata.MD) error {
if md.Len() == 0 {
return nil
}
if s.getState() == streamDone {
return ErrIllegalHeaderWrite
}
s.hdrMu.Lock()
s.trailer = metadata.Join(s.trailer, md)
s.hdrMu.Unlock()
return nil
}
func (s *ServerStream) requestRead(n int) {
s.st.adjustWindow(s, uint32(n))
}
func (s *ServerStream) updateWindow(n int) {
s.st.updateWindow(s, uint32(n))
}

View File

@@ -0,0 +1,878 @@
/*
*
* Copyright 2014 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package transport defines and implements message oriented communication
// channel to complete various transactions (e.g., an RPC). It is meant for
// grpc-internal usage and is not intended to be imported directly by users.
package transport
import (
"context"
"errors"
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"time"
"unsafe"
"golang.org/x/net/http2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/channelz"
"google.golang.org/grpc/internal/envconfig"
imem "google.golang.org/grpc/internal/mem"
"google.golang.org/grpc/internal/transport/internal"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/mem"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
"google.golang.org/grpc/tap"
)
const (
logLevel = 2
// recvMsgSize estimates the memory overhead of a recvMsg in the backlog.
// It accounts for the recvMsg struct itself and the slice header of the
// underlying buffer's data.
recvMsgSize = int(unsafe.Sizeof(recvMsg{}) + unsafe.Sizeof([]byte{}))
// utilizationFactor controls when we consider memory utilization acceptable.
// When backlogHeapSize / payloadSize <= utilizationFactor (meaning at least
// 50% of the heap memory is actual payload data), compaction is skipped.
utilizationFactor = 2
)
var (
// compactionThreshold is approx 57KB (on 64-bit systems). It allows
// accumulating up to 1024 1-byte payloads before triggering compaction.
//
// Because individual payloads <= 1024 bytes are allocated on the heap
// outside mem.BufferPool, waiting for at least 1024 bytes to accumulate
// ensures that compaction coalesces those small heap allocations into a
// single large buffer from mem.BufferPool, enabling buffer reuse while
// avoiding frequent copying for small bursts of frames.
compactionThreshold = imem.BufferPoolingThreshold * (recvMsgSize + 1)
)
func init() {
internal.TimeNowFunc = func() int64 { return time.Now().UnixNano() }
}
// recvMsg represents the received msg from the transport. All transport
// protocol specific info has been removed.
type recvMsg struct {
buffer mem.Buffer
// nil: received some data
// io.EOF: stream is completed. data is nil.
// other non-nil error: transport failure. data is nil.
err error
}
// recvBuffer is an unbounded channel of recvMsg structs.
//
// Note: recvBuffer differs from buffer.Unbounded in that it provides in-place
// value initialization (via init) to avoid struct pointer allocations on
// streams, and automatically frees pooled mem.Buffer payloads when stream
// errors occur.
type recvBuffer struct {
c chan recvMsg
mu sync.Mutex
backlog []recvMsg
// uncompactedSuffixLen tracks the number of consecutive data messages at
// the tail of backlog that have not been compacted.
uncompactedSuffixLen int
// uncompactedBytes tracks the total payload bytes across the trailing
// uncompactedSuffixLen messages.
uncompactedBytes int
err error
bufPool mem.BufferPool
}
// init allows a recvBuffer to be initialized in-place, which is useful
// for resetting a buffer or for avoiding a heap allocation when the buffer
// is embedded in another struct.
func (b *recvBuffer) init(pool mem.BufferPool) {
b.c = make(chan recvMsg, 1)
b.bufPool = pool
}
func (b *recvBuffer) put(r recvMsg) {
b.mu.Lock()
defer b.mu.Unlock()
if b.err != nil {
// drop the buffer on the floor. Since b.err is not nil, any subsequent reads
// will always return an error, making this buffer inaccessible.
r.buffer.Free()
// An error had occurred earlier, don't accept more
// data or errors.
return
}
b.err = r.err
if len(b.backlog) == 0 {
select {
case b.c <- r:
return
default:
}
}
b.backlog = append(b.backlog, r)
b.compactBacklogLocked(r)
}
func (b *recvBuffer) compactBacklogLocked(r recvMsg) {
if !envconfig.EnableReceiveBufferCompaction {
return
}
if r.buffer == nil {
b.uncompactedBytes = 0
b.uncompactedSuffixLen = 0
return
}
b.uncompactedSuffixLen++
b.uncompactedBytes += r.buffer.Len()
backlogHeapSize := b.uncompactedSuffixLen*recvMsgSize + b.uncompactedBytes
// If the memory overhead is less than 50% of the heap usage (e.g., because
// a large DATA frame arrived), the average message size in the suffix is
// large enough that memory bloat is not a concern. Reset suffix tracking.
if backlogHeapSize <= utilizationFactor*b.uncompactedBytes {
b.uncompactedBytes = 0
b.uncompactedSuffixLen = 0
return
}
// Avoid compacting too frequently for short bursts of small frames.
// Wait until we have accumulated at least ~1024 small messages (~57 KB).
if backlogHeapSize <= compactionThreshold {
// Still can accumulate more payloads.
return
}
// Since the memory utilization is less than 50%, the average payload size
// of each recvMsg must be less than recvMsgSize (approx 56 bytes).
// In the worst case for bytes copied (where the average payload is just
// below recvMsgSize), compaction will occur once every:
// compactionThreshold / (recvMsgSize + avg_payload) = ~520 messages,
// copying ~29KB of data.
start := 0
newBuf := b.bufPool.Get(b.uncompactedBytes)
startIdx := len(b.backlog) - b.uncompactedSuffixLen
for i := startIdx; i < len(b.backlog); i++ {
m := b.backlog[i]
b.backlog[i] = recvMsg{}
start += copy((*newBuf)[start:], m.buffer.ReadOnlyData())
m.buffer.Free()
}
b.backlog[startIdx] = recvMsg{
buffer: mem.NewBuffer(newBuf, b.bufPool),
}
b.backlog = b.backlog[:startIdx+1]
// After compaction, the suffix is replaced with a single message containing
// the combined payload. The new utilization is close to 1.0 (overhead of
// one recvMsg relative to the large compacted payload), which is well
// below the utilization factor of 2.
b.uncompactedBytes = 0
b.uncompactedSuffixLen = 0
}
func (b *recvBuffer) load() {
b.mu.Lock()
if len(b.backlog) > 0 {
select {
case b.c <- b.backlog[0]:
// backlog[0] is only part of the tracked uncompacted suffix if the
// entire backlog currently consists of the suffix. If an earlier
// compaction or reset occurred, backlog[0] is already compacted.
if envconfig.EnableReceiveBufferCompaction && b.uncompactedSuffixLen == len(b.backlog) {
b.uncompactedSuffixLen--
b.uncompactedBytes -= b.backlog[0].buffer.Len()
}
b.backlog[0] = recvMsg{}
b.backlog = b.backlog[1:]
default:
}
}
b.mu.Unlock()
}
// get returns the channel that receives a recvMsg in the buffer.
//
// Upon receipt of a recvMsg, the caller should call load to send another
// recvMsg onto the channel if there is any.
func (b *recvBuffer) get() <-chan recvMsg {
return b.c
}
// recvBufferReader implements io.Reader interface to read the data from
// recvBuffer.
type recvBufferReader struct {
_ noCopy
clientStream *ClientStream // The client transport stream is closed with a status representing ctx.Err() and nil trailer metadata.
ctx context.Context
ctxDone <-chan struct{} // cache of ctx.Done() (for performance).
recv *recvBuffer
last mem.Buffer // Stores the remaining data in the previous calls.
err error
}
func (r *recvBufferReader) ReadMessageHeader(header []byte) (n int, err error) {
if r.err != nil {
return 0, r.err
}
if r.last != nil {
n, r.last = mem.ReadUnsafe(header, r.last)
return n, nil
}
if r.clientStream != nil {
n, r.err = r.readMessageHeaderClient(header)
} else {
n, r.err = r.readMessageHeader(header)
}
return n, r.err
}
// Read reads the next n bytes from last. If last is drained, it tries to read
// additional data from recv. It blocks if there no additional data available in
// recv. If Read returns any non-nil error, it will continue to return that
// error.
func (r *recvBufferReader) Read(n int) (buf mem.Buffer, err error) {
if r.err != nil {
return nil, r.err
}
if r.last != nil {
buf = r.last
if r.last.Len() > n {
buf, r.last = mem.SplitUnsafe(buf, n)
} else {
r.last = nil
}
return buf, nil
}
if r.clientStream != nil {
buf, r.err = r.readClient(n)
} else {
buf, r.err = r.read(n)
}
return buf, r.err
}
func (r *recvBufferReader) readMessageHeader(header []byte) (n int, err error) {
select {
case <-r.ctxDone:
return 0, ContextErr(r.ctx.Err())
case m := <-r.recv.get():
return r.readMessageHeaderAdditional(m, header)
}
}
func (r *recvBufferReader) read(n int) (buf mem.Buffer, err error) {
select {
case <-r.ctxDone:
return nil, ContextErr(r.ctx.Err())
case m := <-r.recv.get():
return r.readAdditional(m, n)
}
}
func (r *recvBufferReader) readMessageHeaderClient(header []byte) (n int, err error) {
// If the context is canceled, then closes the stream with nil metadata.
// closeStream writes its error parameter to r.recv as a recvMsg.
// r.readAdditional acts on that message and returns the necessary error.
select {
case <-r.ctxDone:
// Note that this adds the ctx error to the end of recv buffer, and
// reads from the head. This will delay the error until recv buffer is
// empty, thus will delay ctx cancellation in Recv().
//
// It's done this way to fix a race between ctx cancel and trailer. The
// race was, stream.Recv() may return ctx error if ctxDone wins the
// race, but stream.Trailer() may return a non-nil md because the stream
// was not marked as done when trailer is received. This closeStream
// call will mark stream as done, thus fix the race.
//
// TODO: delaying ctx error seems like a unnecessary side effect. What
// we really want is to mark the stream as done, and return ctx error
// faster.
r.clientStream.Close(ContextErr(r.ctx.Err()))
m := <-r.recv.get()
return r.readMessageHeaderAdditional(m, header)
case m := <-r.recv.get():
return r.readMessageHeaderAdditional(m, header)
}
}
func (r *recvBufferReader) readClient(n int) (buf mem.Buffer, err error) {
// If the context is canceled, then closes the stream with nil metadata.
// closeStream writes its error parameter to r.recv as a recvMsg.
// r.readAdditional acts on that message and returns the necessary error.
select {
case <-r.ctxDone:
// Note that this adds the ctx error to the end of recv buffer, and
// reads from the head. This will delay the error until recv buffer is
// empty, thus will delay ctx cancellation in Recv().
//
// It's done this way to fix a race between ctx cancel and trailer. The
// race was, stream.Recv() may return ctx error if ctxDone wins the
// race, but stream.Trailer() may return a non-nil md because the stream
// was not marked as done when trailer is received. This closeStream
// call will mark stream as done, thus fix the race.
//
// TODO: delaying ctx error seems like a unnecessary side effect. What
// we really want is to mark the stream as done, and return ctx error
// faster.
r.clientStream.Close(ContextErr(r.ctx.Err()))
m := <-r.recv.get()
return r.readAdditional(m, n)
case m := <-r.recv.get():
return r.readAdditional(m, n)
}
}
func (r *recvBufferReader) readMessageHeaderAdditional(m recvMsg, header []byte) (n int, err error) {
r.recv.load()
if m.err != nil {
if m.buffer != nil {
m.buffer.Free()
}
return 0, m.err
}
n, r.last = mem.ReadUnsafe(header, m.buffer)
return n, nil
}
func (r *recvBufferReader) readAdditional(m recvMsg, n int) (b mem.Buffer, err error) {
r.recv.load()
if m.err != nil {
if m.buffer != nil {
m.buffer.Free()
}
return nil, m.err
}
if m.buffer.Len() > n {
m.buffer, r.last = mem.SplitUnsafe(m.buffer, n)
}
return m.buffer, nil
}
type streamState uint32
const (
streamActive streamState = iota
streamWriteDone // EndStream sent
streamReadDone // EndStream received
streamDone // the entire stream is finished.
)
// Stream represents an RPC in the transport layer.
type Stream struct {
ctx context.Context // the associated context of the stream
method string // the associated RPC method of the stream
recvCompress string
sendCompress string
readRequester readRequester
// contentSubtype is the content-subtype for requests.
// this must be lowercase or the behavior is undefined.
contentSubtype string
trailer metadata.MD // the key-value map of trailer metadata.
// Non-pointer fields are at the end to optimize GC performance.
state streamState
id uint32
buf recvBuffer
trReader transportReader
fc inFlow
wq writeQuota
}
// readRequester is used to state application's intentions to read data. This
// is used to adjust flow control, if needed.
type readRequester interface {
requestRead(int)
}
func (s *Stream) swapState(st streamState) streamState {
return streamState(atomic.SwapUint32((*uint32)(&s.state), uint32(st)))
}
func (s *Stream) compareAndSwapState(oldState, newState streamState) bool {
return atomic.CompareAndSwapUint32((*uint32)(&s.state), uint32(oldState), uint32(newState))
}
func (s *Stream) getState() streamState {
return streamState(atomic.LoadUint32((*uint32)(&s.state)))
}
// Trailer returns the cached trailer metadata. Note that if it is not called
// after the entire stream is done, it could return an empty MD.
// It can be safely read only after stream has ended that is either read
// or write have returned io.EOF.
func (s *Stream) Trailer() metadata.MD {
return s.trailer.Copy()
}
// Context returns the context of the stream.
func (s *Stream) Context() context.Context {
return s.ctx
}
// Method returns the method for the stream.
func (s *Stream) Method() string {
return s.method
}
func (s *Stream) write(m recvMsg) {
s.buf.put(m)
}
// ReadMessageHeader reads data into the provided header slice from the stream.
// It first checks if there was an error during a previous read operation and
// returns it if present. It then requests a read operation for the length of
// the header. It continues to read from the stream until the entire header
// slice is filled or an error occurs. If an `io.EOF` error is encountered with
// partially read data, it is converted to `io.ErrUnexpectedEOF` to indicate an
// unexpected end of the stream. The method returns any error encountered during
// the read process or nil if the header was successfully read.
func (s *Stream) ReadMessageHeader(header []byte) (err error) {
// Don't request a read if there was an error earlier
if er := s.trReader.er; er != nil {
return er
}
s.readRequester.requestRead(len(header))
bytesRead := 0
for len(header) != 0 {
n, err := s.trReader.ReadMessageHeader(header)
bytesRead += n
header = header[n:]
if len(header) == 0 {
err = nil
}
if err != nil {
if bytesRead > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
s.trReader.er = err
}
return err
}
}
return nil
}
// ceil returns the ceil after dividing the numerator and denominator while
// avoiding integer overflows.
func ceil(numerator, denominator int) int {
if numerator == 0 {
return 0
}
return (numerator-1)/denominator + 1
}
// Read reads n bytes from the wire for this stream.
func (s *Stream) read(n int) (data mem.BufferSlice, err error) {
// Don't request a read if there was an error earlier
if er := s.trReader.er; er != nil {
return nil, er
}
// gRPC Go accepts data frames with a maximum length of 16KB. Larger
// messages must be split into multiple frames. We pre-allocate the
// buffer to avoid resizing during the read loop, but cap the initial
// capacity to 128 frames (2MB) to prevent over-allocation or panics
// when reading extremely large streams.
allocCap := min(ceil(n, http2MaxFrameLen), 128)
data = make(mem.BufferSlice, 0, allocCap)
s.readRequester.requestRead(n)
bytesRead := 0
for n != 0 {
buf, err := s.trReader.Read(n)
var bufLen int
if buf != nil {
bufLen = buf.Len()
}
bytesRead += bufLen
n -= bufLen
if n == 0 {
err = nil
}
if err != nil {
if bytesRead > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
s.trReader.er = err
}
data.Free()
return nil, err
}
data = append(data, buf)
}
return data, nil
}
// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
type noCopy struct {
}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
// transportReader reads all the data available for this Stream from the transport and
// passes them into the decoder, which converts them into a gRPC message stream.
// The error is io.EOF when the stream is done or another non-nil error if
// the stream broke.
type transportReader struct {
_ noCopy
// The handler to control the window update procedure for both this
// particular stream and the associated transport.
windowHandler windowHandler
er error
reader recvBufferReader
}
// The handler to control the window update procedure for both this
// particular stream and the associated transport.
type windowHandler interface {
updateWindow(int)
}
func (t *transportReader) ReadMessageHeader(header []byte) (int, error) {
n, err := t.reader.ReadMessageHeader(header)
if err != nil {
t.er = err
return 0, err
}
t.windowHandler.updateWindow(n)
return n, nil
}
func (t *transportReader) Read(n int) (mem.Buffer, error) {
buf, err := t.reader.Read(n)
if err != nil {
t.er = err
return buf, err
}
t.windowHandler.updateWindow(buf.Len())
return buf, nil
}
// GoString is implemented by Stream so context.String() won't
// race when printing %#v.
func (s *Stream) GoString() string {
return fmt.Sprintf("<stream: %p, %v>", s, s.method)
}
// state of transport
type transportState int
const (
reachable transportState = iota
closing
draining
)
// ServerConfig consists of all the configurations to establish a server transport.
type ServerConfig struct {
MaxStreams uint32
ConnectionTimeout time.Duration
Credentials credentials.TransportCredentials
InTapHandle tap.ServerInHandle
StatsHandler stats.Handler
KeepaliveParams keepalive.ServerParameters
KeepalivePolicy keepalive.EnforcementPolicy
InitialWindowSize int32
InitialConnWindowSize int32
WriteBufferSize int
ReadBufferSize int
SharedWriteBuffer bool
ChannelzParent *channelz.Server
MaxHeaderListSize *uint32
HeaderTableSize *uint32
BufferPool mem.BufferPool
StaticWindowSize bool
}
// ConnectOptions covers all relevant options for communicating with the server.
type ConnectOptions struct {
// UserAgent is the application user agent.
UserAgent string
// Dialer specifies how to dial a network address.
Dialer func(context.Context, string) (net.Conn, error)
// FailOnNonTempDialError specifies if gRPC fails on non-temporary dial errors.
FailOnNonTempDialError bool
// PerRPCCredentials stores the PerRPCCredentials required to issue RPCs.
PerRPCCredentials []credentials.PerRPCCredentials
// TransportCredentials stores the Authenticator required to setup a client
// connection. Only one of TransportCredentials and CredsBundle is non-nil.
TransportCredentials credentials.TransportCredentials
// CredsBundle is the credentials bundle to be used. Only one of
// TransportCredentials and CredsBundle is non-nil.
CredsBundle credentials.Bundle
// KeepaliveParams stores the keepalive parameters.
KeepaliveParams keepalive.ClientParameters
// StatsHandlers stores the handler for stats.
StatsHandlers []stats.Handler
// InitialWindowSize sets the initial window size for a stream.
InitialWindowSize int32
// InitialConnWindowSize sets the initial window size for a connection.
InitialConnWindowSize int32
// WriteBufferSize sets the size of write buffer which in turn determines how much data can be batched before it's written on the wire.
WriteBufferSize int
// ReadBufferSize sets the size of read buffer, which in turn determines how much data can be read at most for one read syscall.
ReadBufferSize int
// SharedWriteBuffer indicates whether connections should reuse write buffer
SharedWriteBuffer bool
// ChannelzParent sets the addrConn id which initiated the creation of this client transport.
ChannelzParent *channelz.SubChannel
// MaxHeaderListSize sets the max (uncompressed) size of header list that is prepared to be received.
MaxHeaderListSize *uint32
// The mem.BufferPool to use when reading/writing to the wire.
BufferPool mem.BufferPool
// StaticWindowSize controls whether dynamic window sizing is enabled.
StaticWindowSize bool
}
// WriteOptions provides additional hints and information for message
// transmission.
type WriteOptions struct {
// Last indicates whether this write is the last piece for
// this stream.
Last bool
}
// CallHdr carries the information of a particular RPC.
type CallHdr struct {
// Host specifies the peer's host.
Host string
// Method specifies the operation to perform.
Method string
// SendCompress specifies the compression algorithm applied on
// outbound message.
SendCompress string
// AcceptedCompressors overrides the grpc-accept-encoding header for this
// call. When nil, the transport advertises the default set of registered
// compressors. A non-nil pointer overrides that value (including the empty
// string to advertise none).
AcceptedCompressors *string
// Creds specifies credentials.PerRPCCredentials for a call.
Creds credentials.PerRPCCredentials
// ContentSubtype specifies the content-subtype for a request. For example, a
// content-subtype of "proto" will result in a content-type of
// "application/grpc+proto". The value of ContentSubtype must be all
// lowercase, otherwise the behavior is undefined. See
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
// for more details.
ContentSubtype string
PreviousAttempts int // value of grpc-previous-rpc-attempts header to set
// Authority is used to explicitly override the `:authority` header.
//
// This value comes from one of two sources:
// 1. The `CallAuthority` call option, if specified by the user.
// 2. An override provided by the LB picker (e.g. xDS authority rewriting).
//
// The `CallAuthority` call option always takes precedence over the LB
// picker override.
Authority string
}
// ClientTransport is the common interface for all gRPC client-side transport
// implementations.
type ClientTransport interface {
// Close tears down this transport. Once it returns, the transport
// should not be accessed any more. The caller must make sure this
// is called only once.
Close(err error)
// GracefulClose starts to tear down the transport: the transport will stop
// accepting new RPCs and NewStream will return error. Once all streams are
// finished, the transport will close.
//
// It does not block.
GracefulClose()
// NewStream creates a Stream for an RPC.
NewStream(ctx context.Context, callHdr *CallHdr, handler stats.Handler) (*ClientStream, error)
// Error returns a channel that is closed when some I/O error
// happens. Typically the caller should have a goroutine to monitor
// this in order to take action (e.g., close the current transport
// and create a new one) in error case. It should not return nil
// once the transport is initiated.
Error() <-chan struct{}
// GoAway returns a channel that is closed when ClientTransport
// receives the draining signal from the server (e.g., GOAWAY frame in
// HTTP/2).
GoAway() <-chan struct{}
// GetGoAwayReason returns the reason why GoAway frame was received, along
// with a human readable string with debug info.
GetGoAwayReason() (GoAwayReason, string)
// Peer returns information about the peer associated with the Transport.
// The returned information includes authentication and network address details.
Peer() *peer.Peer
}
// ServerTransport is the common interface for all gRPC server-side transport
// implementations.
//
// Methods may be called concurrently from multiple goroutines, but
// Write methods for a given Stream will be called serially.
type ServerTransport interface {
// HandleStreams receives incoming streams using the given handler.
HandleStreams(context.Context, func(*ServerStream))
// Close tears down the transport. Once it is called, the transport
// should not be accessed any more. All the pending streams and their
// handlers will be terminated asynchronously.
Close(err error)
// Peer returns the peer of the server transport.
Peer() *peer.Peer
// Drain notifies the client this ServerTransport stops accepting new RPCs.
Drain(debugData string)
}
type internalServerTransport interface {
ServerTransport
writeHeader(s *ServerStream, md metadata.MD) error
write(s *ServerStream, hdr []byte, data mem.BufferSlice, opts *WriteOptions) error
writeStatus(s *ServerStream, st *status.Status) error
incrMsgRecv()
adjustWindow(s *ServerStream, n uint32)
updateWindow(s *ServerStream, n uint32)
}
// connectionErrorf creates an ConnectionError with the specified error description.
func connectionErrorf(temp bool, e error, format string, a ...any) ConnectionError {
return ConnectionError{
Desc: fmt.Sprintf(format, a...),
temp: temp,
err: e,
}
}
// ConnectionError is an error that results in the termination of the
// entire connection and the retry of all the active streams.
type ConnectionError struct {
Desc string
temp bool
err error
}
func (e ConnectionError) Error() string {
return fmt.Sprintf("connection error: desc = %q", e.Desc)
}
// Temporary indicates if this connection error is temporary or fatal.
func (e ConnectionError) Temporary() bool {
return e.temp
}
// Origin returns the original error of this connection error.
func (e ConnectionError) Origin() error {
// Never return nil error here.
// If the original error is nil, return itself.
if e.err == nil {
return e
}
return e.err
}
// Unwrap returns the original error of this connection error or nil when the
// origin is nil.
func (e ConnectionError) Unwrap() error {
return e.err
}
var (
// ErrConnClosing indicates that the transport is closing.
ErrConnClosing = connectionErrorf(true, nil, "transport is closing")
// errStreamDrain indicates that the stream is rejected because the
// connection is draining. This could be caused by goaway or balancer
// removing the address.
errStreamDrain = status.Error(codes.Unavailable, "the connection is draining")
// errStreamDone is returned from write at the client side to indicate application
// layer of an error.
errStreamDone = errors.New("the stream is done")
// StatusGoAway indicates that the server sent a GOAWAY that included this
// stream's ID in unprocessed RPCs.
statusGoAway = status.New(codes.Unavailable, "the stream is rejected because server is draining the connection")
)
// GoAwayReason contains the reason for the GoAway frame received.
type GoAwayReason uint8
const (
// GoAwayInvalid indicates that no GoAway frame is received.
GoAwayInvalid GoAwayReason = 0
// GoAwayNoReason is the default value when GoAway frame is received.
GoAwayNoReason GoAwayReason = 1
// GoAwayTooManyPings indicates that a GoAway frame with
// ErrCodeEnhanceYourCalm was received and that the debug data said
// "too_many_pings".
GoAwayTooManyPings GoAwayReason = 2
)
// GoAwayInfo contains metadata about why a connection was closed.
type GoAwayInfo struct {
// Reason is the parsed reason for an HTTP/2 GOAWAY frame.
Reason GoAwayReason
// GoAwayCode is the raw HTTP/2 error code received in a GOAWAY frame.
GoAwayCode http2.ErrCode
// Err is the underlying error that caused the connection to close. It is
// populated if the connection was closed due to a socket error or context
// cancellation without receiving a GOAWAY frame. If the connection was
// closed due to a GOAWAY frame, this field will be nil.
Err error
}
// OnCloseFunc is a callback invoked when a ClientTransport closes.
type OnCloseFunc func(GoAwayInfo)
// ContextErr converts the error from context package into a status error.
func ContextErr(err error) error {
switch err {
case context.DeadlineExceeded:
return status.Error(codes.DeadlineExceeded, err.Error())
case context.Canceled:
return status.Error(codes.Canceled, err.Error())
}
return status.Errorf(codes.Internal, "Unexpected error from context packet: %v", err)
}