github.com/avenga/couper@v1.12.2/handler/transport/backend.go (about)

     1  package transport
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"encoding/base64"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptrace"
    10  	"net/url"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/hashicorp/hcl/v2"
    16  	"github.com/hashicorp/hcl/v2/hclsyntax"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/zclconf/go-cty/cty"
    19  	"go.opentelemetry.io/otel/attribute"
    20  	"go.opentelemetry.io/otel/metric/instrument"
    21  	"go.opentelemetry.io/otel/metric/unit"
    22  	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
    23  	"go.opentelemetry.io/otel/trace"
    24  
    25  	"github.com/avenga/couper/config"
    26  	hclbody "github.com/avenga/couper/config/body"
    27  	"github.com/avenga/couper/config/request"
    28  	"github.com/avenga/couper/errors"
    29  	"github.com/avenga/couper/eval"
    30  	"github.com/avenga/couper/eval/buffer"
    31  	"github.com/avenga/couper/eval/variables"
    32  	"github.com/avenga/couper/handler/ratelimit"
    33  	"github.com/avenga/couper/handler/validation"
    34  	"github.com/avenga/couper/internal/seetie"
    35  	"github.com/avenga/couper/logging"
    36  	"github.com/avenga/couper/server/writer"
    37  	"github.com/avenga/couper/telemetry"
    38  	"github.com/avenga/couper/telemetry/instrumentation"
    39  	"github.com/avenga/couper/telemetry/provider"
    40  	"github.com/avenga/couper/utils"
    41  )
    42  
    43  var (
    44  	_ http.RoundTripper = &Backend{}
    45  	_ ProbeStateChange  = &Backend{}
    46  	_ seetie.Object     = &Backend{}
    47  )
    48  
    49  type Backend struct {
    50  	context             *hclsyntax.Body
    51  	healthInfo          *HealthInfo
    52  	healthyMu           sync.RWMutex
    53  	logEntry            *logrus.Entry
    54  	name                string
    55  	openAPIValidator    *validation.OpenAPI
    56  	requestAuthorizer   []RequestAuthorizer
    57  	transport           http.RoundTripper
    58  	transportConf       *Config
    59  	transportConfResult Config
    60  	transportOnce       sync.Once
    61  	upstreamLog         *logging.UpstreamLog
    62  }
    63  
    64  // NewBackend creates a new <*Backend> object by the given <*Config>.
    65  func NewBackend(ctx *hclsyntax.Body, tc *Config, opts *BackendOptions, log *logrus.Entry) http.RoundTripper {
    66  	var (
    67  		healthCheck       *config.HealthCheck
    68  		openAPI           *validation.OpenAPI
    69  		requestAuthorizer []RequestAuthorizer
    70  	)
    71  
    72  	if opts != nil {
    73  		healthCheck = opts.HealthCheck
    74  		openAPI = validation.NewOpenAPI(opts.OpenAPI)
    75  		requestAuthorizer = opts.RequestAuthz
    76  	}
    77  
    78  	backend := &Backend{
    79  		context:           ctx,
    80  		healthInfo:        &HealthInfo{Healthy: true, State: StateOk.String()},
    81  		logEntry:          log.WithField("backend", tc.BackendName),
    82  		name:              tc.BackendName,
    83  		openAPIValidator:  openAPI,
    84  		requestAuthorizer: requestAuthorizer,
    85  		transportConf:     tc,
    86  	}
    87  
    88  	backend.upstreamLog = logging.NewUpstreamLog(backend.logEntry, backend, tc.NoProxyFromEnv)
    89  
    90  	distinct := !strings.HasPrefix(tc.BackendName, "anonymous_")
    91  	if distinct && healthCheck != nil {
    92  		NewProbe(backend.logEntry, tc, healthCheck, backend)
    93  	}
    94  
    95  	return backend.upstreamLog
    96  }
    97  
    98  // initOnce ensures synced transport configuration. First request will setup the rate limits, origin, hostname and tls.
    99  func (b *Backend) initOnce(conf *Config) {
   100  	if len(b.transportConf.RateLimits) > 0 {
   101  		b.transport = ratelimit.NewLimiter(NewTransport(conf, b.logEntry), b.transportConf.RateLimits)
   102  	} else {
   103  		b.transport = NewTransport(conf, b.logEntry)
   104  	}
   105  
   106  	b.healthyMu.Lock()
   107  	b.transportConfResult = *conf
   108  	healthy := b.healthInfo.Healthy
   109  	healthState := b.healthInfo.State
   110  	b.healthyMu.Unlock()
   111  
   112  	// race condition, update possible healthy backend with current origin and hostname
   113  	b.OnProbeChange(&HealthInfo{Healthy: healthy, State: healthState})
   114  }
   115  
   116  // RoundTrip implements the <http.RoundTripper> interface.
   117  func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
   118  	ctxBody, _ := req.Context().Value(request.BackendParams).(*hclsyntax.Body)
   119  	if ctxBody == nil {
   120  		ctxBody = b.context
   121  	} else {
   122  		ctxBody = hclbody.MergeBodies(ctxBody, b.context, false)
   123  	}
   124  
   125  	outreq := req.WithContext(context.WithValue(req.Context(), request.BackendName, b.name))
   126  
   127  	// originalReq for token-request retry purposes
   128  	originalReq, err := b.withTokenRequest(outreq)
   129  	if err != nil {
   130  		return nil, errors.BetaBackendTokenRequest.Label(b.name).With(err)
   131  	}
   132  
   133  	var backendVal cty.Value
   134  	hclCtx := eval.ContextFromRequest(outreq).HCLContextSync()
   135  	if v, ok := hclCtx.Variables[variables.Backends]; ok {
   136  		if m, exist := v.AsValueMap()[b.name]; exist {
   137  			hclCtx.Variables[variables.Backend] = m
   138  			backendVal = m
   139  		}
   140  	}
   141  
   142  	if err = b.isUnhealthy(hclCtx, ctxBody); err != nil {
   143  		return &http.Response{
   144  			Request: req, // provide outreq (variable) on error cases
   145  		}, err
   146  	}
   147  
   148  	// Execute before <b.evalTransport()> due to right
   149  	// handling of query-params in the URL attribute.
   150  	if err = eval.ApplyRequestContext(hclCtx, ctxBody, outreq); err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	// TODO: split timing eval
   155  	tc, err := b.evalTransport(hclCtx, ctxBody, outreq)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	// first traffic pins the origin settings to transportConfResult
   161  	b.transportOnce.Do(func() {
   162  		b.initOnce(tc)
   163  	})
   164  
   165  	// use result and apply context timings
   166  	b.healthyMu.RLock()
   167  	tconf := b.transportConfResult
   168  	b.healthyMu.RUnlock()
   169  	tconf.ConnectTimeout = tc.ConnectTimeout
   170  	tconf.TTFBTimeout = tc.TTFBTimeout
   171  	tconf.Timeout = tc.Timeout
   172  
   173  	deadlineErr := b.withTimeout(outreq, &tconf)
   174  
   175  	outreq.URL.Host = tconf.Origin
   176  	outreq.URL.Scheme = tconf.Scheme
   177  	outreq.Host = tconf.Hostname
   178  
   179  	// handler.Proxy marks proxy round-trips since we should not handle headers twice.
   180  	_, isProxyReq := outreq.Context().Value(request.RoundTripProxy).(bool)
   181  
   182  	if !isProxyReq {
   183  		RemoveConnectionHeaders(outreq.Header)
   184  		RemoveHopHeaders(outreq.Header)
   185  	}
   186  
   187  	writer.ModifyAcceptEncoding(outreq.Header)
   188  
   189  	if xff, ok := outreq.Context().Value(request.XFF).(string); ok {
   190  		if xff != "" {
   191  			outreq.Header.Set("X-Forwarded-For", xff)
   192  		} else {
   193  			outreq.Header.Del("X-Forwarded-For")
   194  		}
   195  	}
   196  
   197  	b.withBasicAuth(outreq, hclCtx, ctxBody)
   198  	if err = b.withPathPrefix(outreq, hclCtx, ctxBody); err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	setUserAgent(outreq)
   203  	outreq.Close = false
   204  
   205  	if _, ok := req.Context().Value(request.WebsocketsAllowed).(bool); !ok {
   206  		outreq.Header.Del("Connection")
   207  		outreq.Header.Del("Upgrade")
   208  	}
   209  
   210  	var beresp *http.Response
   211  	if b.openAPIValidator != nil {
   212  		beresp, err = b.openAPIValidate(outreq, &tconf, deadlineErr)
   213  	} else {
   214  		beresp, err = b.innerRoundTrip(outreq, &tconf, deadlineErr)
   215  	}
   216  
   217  	if err != nil {
   218  		if beresp == nil {
   219  			beresp = &http.Response{
   220  				Request: outreq,
   221  			} // provide outreq (variable) on error cases
   222  		}
   223  		if varSync, ok := outreq.Context().Value(request.ContextVariablesSynced).(*eval.SyncedVariables); ok {
   224  			varSync.SetResp(beresp)
   225  		}
   226  		return beresp, err
   227  	}
   228  
   229  	if retry, rerr := b.withRetryTokenRequest(outreq, beresp); rerr != nil {
   230  		return beresp, errors.BetaBackendTokenRequest.Label(b.name).With(rerr)
   231  	} else if retry {
   232  		return b.RoundTrip(originalReq)
   233  	}
   234  
   235  	if !eval.IsUpgradeResponse(outreq, beresp) {
   236  		beresp.Body = logging.NewBytesCountReader(beresp)
   237  		if err = setGzipReader(beresp); err != nil {
   238  			b.upstreamLog.LogEntry().WithContext(outreq.Context()).WithError(err).Error()
   239  		}
   240  	}
   241  
   242  	if !isProxyReq {
   243  		RemoveConnectionHeaders(beresp.Header)
   244  		RemoveHopHeaders(beresp.Header)
   245  	}
   246  
   247  	// Backend response context creates the beresp variables in first place and applies this context
   248  	// to the current beresp obj. Downstream response context evals reading their beresp variable values
   249  	// from this result.
   250  	evalCtx := eval.ContextFromRequest(outreq)
   251  	var bereqV, berespV cty.Value
   252  	var reqName string
   253  	evalCtx, reqName, bereqV, berespV = evalCtx.WithBeresp(beresp, backendVal)
   254  
   255  	clfValue, err := eval.EvalCustomLogFields(evalCtx.HCLContext(), ctxBody)
   256  	if err != nil {
   257  		logError, _ := outreq.Context().Value(request.LogCustomUpstreamError).(*error)
   258  		*logError = err
   259  	} else if clfValue != cty.NilVal {
   260  		logValue, _ := outreq.Context().Value(request.LogCustomUpstreamValue).(*cty.Value)
   261  		*logValue = clfValue
   262  	}
   263  
   264  	err = eval.ApplyResponseContext(evalCtx.HCLContext(), ctxBody, beresp)
   265  
   266  	if varSync, ok := outreq.Context().Value(request.ContextVariablesSynced).(*eval.SyncedVariables); ok {
   267  		varSync.Set(outreq.Context(), reqName, bereqV, berespV)
   268  	}
   269  
   270  	return beresp, err
   271  }
   272  
   273  func (b *Backend) openAPIValidate(req *http.Request, tc *Config, deadlineErr <-chan error) (*http.Response, error) {
   274  	requestValidationInput, err := b.openAPIValidator.ValidateRequest(req)
   275  	if err != nil {
   276  		return nil, errors.BackendOpenapiValidation.Label(b.name).With(err)
   277  	}
   278  
   279  	beresp, err := b.innerRoundTrip(req, tc, deadlineErr)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  
   284  	if err = b.openAPIValidator.ValidateResponse(beresp, requestValidationInput); err != nil {
   285  		return beresp, errors.BackendOpenapiValidation.Label(b.name).With(err).Status(http.StatusBadGateway)
   286  	}
   287  
   288  	return beresp, nil
   289  }
   290  
   291  func (b *Backend) innerRoundTrip(req *http.Request, tc *Config, deadlineErr <-chan error) (*http.Response, error) {
   292  	span := trace.SpanFromContext(req.Context())
   293  	span.SetAttributes(telemetry.KeyOrigin.String(tc.Origin))
   294  	span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(req)...)
   295  
   296  	spanMsg := "backend"
   297  	if b.name != "" {
   298  		spanMsg += "." + b.name
   299  	}
   300  
   301  	meter := provider.Meter(instrumentation.BackendInstrumentationName)
   302  	counter, _ := meter.SyncInt64().Counter(
   303  		instrumentation.BackendRequest,
   304  		instrument.WithDescription(string(unit.Dimensionless)),
   305  	)
   306  	duration, _ := meter.SyncFloat64().Histogram(
   307  		instrumentation.BackendRequestDuration,
   308  		instrument.WithDescription(string(unit.Dimensionless)),
   309  	)
   310  
   311  	attrs := []attribute.KeyValue{
   312  		attribute.String("backend_name", tc.BackendName),
   313  		attribute.String("hostname", tc.Hostname),
   314  		attribute.String("method", req.Method),
   315  		attribute.String("origin", tc.Origin),
   316  	}
   317  
   318  	start := time.Now()
   319  	span.AddEvent(spanMsg + ".request")
   320  	beresp, err := b.transport.RoundTrip(req)
   321  	span.AddEvent(spanMsg + ".response")
   322  	endSeconds := time.Since(start).Seconds()
   323  
   324  	statusKey := attribute.Key("code")
   325  	if beresp != nil {
   326  		attrs = append(attrs, statusKey.Int(beresp.StatusCode))
   327  	}
   328  
   329  	defer counter.Add(req.Context(), 1, attrs...)
   330  	defer duration.Record(req.Context(), endSeconds, attrs...)
   331  
   332  	if err != nil {
   333  		select {
   334  		case derr := <-deadlineErr:
   335  			if derr != nil {
   336  				return nil, derr
   337  			}
   338  		default:
   339  			if _, ok := err.(*errors.Error); ok {
   340  				return nil, err
   341  			}
   342  
   343  			return nil, errors.Backend.Label(b.name).With(err)
   344  		}
   345  	}
   346  
   347  	return beresp, nil
   348  }
   349  
   350  func (b *Backend) withTokenRequest(req *http.Request) (*http.Request, error) {
   351  	if b.requestAuthorizer == nil {
   352  		return nil, nil
   353  	}
   354  
   355  	// Reset for upstream transport; prevent mixing values.
   356  	// requestAuthorizer will have their own backend configuration.
   357  	ctx := context.WithValue(req.Context(), request.BackendParams, nil)
   358  
   359  	var cancel context.CancelFunc
   360  	ctx, cancel = context.WithCancel(ctx)
   361  	defer cancel()
   362  
   363  	originalReq := req.Clone(req.Context())
   364  
   365  	// WithContext() instead of Clone() due to header-map modification.
   366  	req = req.WithContext(ctx)
   367  
   368  	errorsCh := make(chan error, len(b.requestAuthorizer))
   369  	for _, authorizer := range b.requestAuthorizer {
   370  		err := authorizer.GetToken(req)
   371  		if err != nil {
   372  			return originalReq, err
   373  		}
   374  
   375  		go func(ra RequestAuthorizer, r *http.Request) {
   376  			errorsCh <- ra.GetToken(r)
   377  		}(authorizer, req)
   378  	}
   379  
   380  	var err error
   381  	for i := 0; i < len(b.requestAuthorizer); i++ {
   382  		err = <-errorsCh
   383  		if err != nil {
   384  			break
   385  		}
   386  	}
   387  	return originalReq, err
   388  }
   389  
   390  func (b *Backend) withRetryTokenRequest(req *http.Request, res *http.Response) (bool, error) {
   391  	if len(b.requestAuthorizer) == 0 {
   392  		return false, nil
   393  	}
   394  
   395  	var retry bool
   396  	for _, ra := range b.requestAuthorizer {
   397  		r, err := ra.RetryWithToken(req, res)
   398  		if err != nil {
   399  			return false, err
   400  		}
   401  		if r {
   402  			retry = true
   403  			break
   404  		}
   405  	}
   406  	return retry, nil
   407  }
   408  
   409  func (b *Backend) withPathPrefix(req *http.Request, evalCtx *hcl.EvalContext, hclContext *hclsyntax.Body) error {
   410  	if pathPrefix := b.getAttribute(evalCtx, "path_prefix", hclContext); pathPrefix != "" {
   411  		// TODO: Check for a valid absolute path
   412  		if i := strings.Index(pathPrefix, "#"); i >= 0 {
   413  			return errors.Configuration.Messagef("path_prefix attribute: invalid fragment found in %q", pathPrefix)
   414  		} else if i = strings.Index(pathPrefix, "?"); i >= 0 {
   415  			return errors.Configuration.Messagef("path_prefix attribute: invalid query string found in %q", pathPrefix)
   416  		}
   417  
   418  		req.URL.Path = utils.JoinPath("/", pathPrefix, req.URL.Path)
   419  	}
   420  
   421  	return nil
   422  }
   423  
   424  func (b *Backend) withBasicAuth(req *http.Request, evalCtx *hcl.EvalContext, hclContext *hclsyntax.Body) {
   425  	if creds := b.getAttribute(evalCtx, "basic_auth", hclContext); creds != "" {
   426  		auth := base64.StdEncoding.EncodeToString([]byte(creds))
   427  		req.Header.Set("Authorization", "Basic "+auth)
   428  	}
   429  }
   430  
   431  func (b *Backend) getAttribute(evalContext *hcl.EvalContext, name string, hclContext *hclsyntax.Body) string {
   432  	attrVal, err := eval.ValueFromBodyAttribute(evalContext, hclContext, name)
   433  	if err != nil {
   434  		b.upstreamLog.LogEntry().WithError(errors.Evaluation.Label(b.name).With(err))
   435  	}
   436  	return seetie.ValueToString(attrVal)
   437  }
   438  
   439  func (b *Backend) withTimeout(req *http.Request, conf *Config) <-chan error {
   440  	timeout := conf.Timeout
   441  	ws := false
   442  	if to, ok := req.Context().Value(request.WebsocketsTimeout).(time.Duration); ok {
   443  		timeout = to
   444  		ws = true
   445  	}
   446  
   447  	errCh := make(chan error, 1)
   448  	if timeout+conf.TTFBTimeout <= 0 {
   449  		return errCh
   450  	}
   451  
   452  	ctx, cancel := context.WithCancel(context.WithValue(req.Context(), request.ConnectTimeout, conf.ConnectTimeout))
   453  
   454  	downstreamTrace := httptrace.ContextClientTrace(ctx) // e.g. log-timings
   455  
   456  	ttfbTimeout := make(chan time.Time, 1) // size to always cleanup related go-routine
   457  	ttfbTimer := time.NewTimer(conf.TTFBTimeout)
   458  	ctxTrace := &httptrace.ClientTrace{
   459  		WroteRequest: func(info httptrace.WroteRequestInfo) {
   460  			if downstreamTrace != nil && downstreamTrace.WroteRequest != nil {
   461  				downstreamTrace.WroteRequest(info)
   462  			}
   463  
   464  			if conf.TTFBTimeout <= 0 {
   465  				return
   466  			}
   467  
   468  			go func(c context.Context, timeoutCh chan time.Time) {
   469  				ttfbTimer.Reset(conf.TTFBTimeout)
   470  				select {
   471  				case <-c.Done():
   472  					ttfbTimer.Stop()
   473  					select {
   474  					case <-ttfbTimer.C:
   475  					default:
   476  					}
   477  				case t := <-ttfbTimer.C:
   478  					// buffered, no select done required
   479  					timeoutCh <- t
   480  				}
   481  			}(ctx, ttfbTimeout)
   482  		},
   483  		GotFirstResponseByte: func() {
   484  			if downstreamTrace != nil && downstreamTrace.GotFirstResponseByte != nil {
   485  				downstreamTrace.GotFirstResponseByte()
   486  			}
   487  			ttfbTimer.Stop()
   488  		},
   489  	}
   490  
   491  	*req = *req.WithContext(httptrace.WithClientTrace(ctx, ctxTrace))
   492  
   493  	go func(c context.Context, cancelFn func(), ec chan error) {
   494  		defer cancelFn()
   495  		deadline := make(<-chan time.Time)
   496  		if timeout > 0 {
   497  			deadlineTimer := time.NewTimer(timeout)
   498  			deadline = deadlineTimer.C
   499  			defer deadlineTimer.Stop()
   500  		}
   501  		select {
   502  		case <-deadline:
   503  			if ws {
   504  				ec <- errors.BackendTimeout.Label(b.name).Message("websockets: deadline exceeded")
   505  				return
   506  			}
   507  			ec <- errors.BackendTimeout.Label(b.name).Message("deadline exceeded")
   508  			return
   509  		case <-ttfbTimeout:
   510  			ec <- errors.BackendTimeout.Label(b.name).Message("timeout awaiting response headers")
   511  		case <-c.Done():
   512  			return
   513  		}
   514  	}(ctx, cancel, errCh)
   515  	return errCh
   516  }
   517  
   518  func (b *Backend) evalTransport(httpCtx *hcl.EvalContext, params *hclsyntax.Body, req *http.Request) (*Config, error) {
   519  	log := b.upstreamLog.LogEntry()
   520  
   521  	var origin, hostname, proxyURL string
   522  	var connectTimeout, ttfbTimeout, timeout string
   523  	type pair struct {
   524  		attrName string
   525  		target   *string
   526  	}
   527  
   528  	for _, p := range []pair{
   529  		{"origin", &origin},
   530  		{"hostname", &hostname},
   531  		{"proxy", &proxyURL},
   532  		// dynamic timings
   533  		{"connect_timeout", &connectTimeout},
   534  		{"ttfb_timeout", &ttfbTimeout},
   535  		{"timeout", &timeout},
   536  	} {
   537  		if v, err := eval.ValueFromBodyAttribute(httpCtx, params, p.attrName); err != nil {
   538  			log.WithError(errors.Evaluation.Label(b.name).With(err)).Error()
   539  		} else if v != cty.NilVal {
   540  			*p.target = seetie.ValueToString(v)
   541  		}
   542  	}
   543  
   544  	originURL, parseErr := url.Parse(origin)
   545  	if parseErr != nil {
   546  		return nil, errors.Configuration.Label(b.name).With(parseErr)
   547  	} else if strings.HasPrefix(originURL.Host, originURL.Scheme+":") {
   548  		return nil, errors.Configuration.Label(b.name).
   549  			Messagef("invalid url: %s", originURL.String())
   550  	} else if origin == "" {
   551  		originURL = req.URL
   552  	}
   553  
   554  	if hostname == "" {
   555  		hostname = originURL.Host
   556  	}
   557  
   558  	if !originURL.IsAbs() || originURL.Hostname() == "" {
   559  		return nil, errors.Configuration.Label(b.name).
   560  			Messagef("the origin attribute has to contain an absolute URL with a valid hostname: %q", origin)
   561  	}
   562  
   563  	return b.transportConf.
   564  		WithTarget(originURL.Scheme, originURL.Host, hostname, proxyURL).
   565  		WithTimings(connectTimeout, ttfbTimeout, timeout, log), nil
   566  }
   567  
   568  func (b *Backend) isUnhealthy(ctx *hcl.EvalContext, params *hclsyntax.Body) error {
   569  	val, err := eval.ValueFromBodyAttribute(ctx, params, "use_when_unhealthy")
   570  	if err != nil {
   571  		return err
   572  	}
   573  
   574  	var useUnhealthy bool
   575  	if val.Type() == cty.Bool {
   576  		useUnhealthy = val.True()
   577  	} // else not set
   578  
   579  	b.healthyMu.RLock()
   580  	defer b.healthyMu.RUnlock()
   581  
   582  	if b.healthInfo.Healthy || useUnhealthy {
   583  		return nil
   584  	}
   585  
   586  	return errors.BackendUnhealthy
   587  }
   588  
   589  func (b *Backend) OnProbeChange(info *HealthInfo) {
   590  	b.healthyMu.Lock()
   591  	b.healthInfo = info
   592  	b.healthyMu.Unlock()
   593  }
   594  
   595  func (b *Backend) Value() cty.Value {
   596  	b.healthyMu.RLock()
   597  	defer b.healthyMu.RUnlock()
   598  
   599  	var tokens map[string]interface{}
   600  	for _, auth := range b.requestAuthorizer {
   601  		if name, v := auth.value(); v != "" {
   602  			if tokens == nil {
   603  				tokens = make(map[string]interface{})
   604  			}
   605  			tokens[name] = v
   606  		}
   607  	}
   608  
   609  	result := map[string]interface{}{
   610  		"health": map[string]interface{}{
   611  			"healthy": b.healthInfo.Healthy,
   612  			"error":   b.healthInfo.Error,
   613  			"state":   b.healthInfo.State,
   614  		},
   615  		"hostname":        b.transportConfResult.Hostname,
   616  		"name":            b.name, // mandatory
   617  		"origin":          b.transportConfResult.Origin,
   618  		"connect_timeout": b.transportConfResult.ConnectTimeout.String(),
   619  		"ttfb_timeout":    b.transportConfResult.TTFBTimeout.String(),
   620  		"timeout":         b.transportConfResult.Timeout.String(),
   621  	}
   622  
   623  	if tokens != nil {
   624  		result["beta_tokens"] = tokens
   625  		if token, ok := tokens["default"]; ok {
   626  			result["beta_token"] = token
   627  		}
   628  	}
   629  
   630  	return seetie.GoToValue(result)
   631  }
   632  
   633  // setUserAgent sets an empty one if none is present or empty
   634  // to prevent the go http defaultUA gets written.
   635  func setUserAgent(outreq *http.Request) {
   636  	if ua := outreq.Header.Get("User-Agent"); ua == "" {
   637  		outreq.Header.Set("User-Agent", "")
   638  	}
   639  }
   640  
   641  // setGzipReader will set the gzip.Reader for Content-Encoding gzip.
   642  // Invalid header reads will reset the response.Body and return the related error.
   643  func setGzipReader(beresp *http.Response) error {
   644  	if strings.ToLower(beresp.Header.Get(writer.ContentEncodingHeader)) != writer.GzipName {
   645  		return nil
   646  	}
   647  
   648  	bufOpt := beresp.Request.Context().Value(request.BufferOptions).(buffer.Option)
   649  	if !bufOpt.Response() {
   650  		return nil
   651  	}
   652  
   653  	var src io.Reader
   654  	src, err := gzip.NewReader(beresp.Body)
   655  	if err != nil {
   656  		return errors.Backend.With(err).Message("body reset")
   657  	}
   658  
   659  	beresp.Header.Del(writer.ContentEncodingHeader)
   660  	beresp.Header.Del("Content-Length")
   661  	beresp.Body = eval.NewReadCloser(src, beresp.Body)
   662  	return nil
   663  }
   664  
   665  // RemoveConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
   666  // See RFC 7230, section 6.1
   667  func RemoveConnectionHeaders(h http.Header) {
   668  	for _, f := range h["Connection"] {
   669  		for _, sf := range strings.Split(f, ",") {
   670  			if sf = strings.TrimSpace(sf); sf != "" {
   671  				h.Del(sf)
   672  			}
   673  		}
   674  	}
   675  }
   676  
   677  func RemoveHopHeaders(header http.Header) {
   678  	for _, h := range HopHeaders {
   679  		hv := header.Get(h)
   680  		if hv == "" {
   681  			continue
   682  		}
   683  		if h == "Te" && hv == "trailers" {
   684  			// Issue 21096: tell backend applications that
   685  			// care about trailer support that we support
   686  			// trailers. (We do, but we don't go out of
   687  			// our way to advertise that unless the
   688  			// incoming client request thought it was
   689  			// worth mentioning)
   690  			continue
   691  		}
   692  		header.Del(h)
   693  	}
   694  }
   695  
   696  // HopHeaders Hop-by-hop headers. These are removed when sent to the backend.
   697  // As of RFC 7230, hop-by-hop headers are required to appear in the
   698  // Connection header field. These are the headers defined by the
   699  // obsoleted RFC 2616 (section 13.5.1) and are used for backward
   700  // compatibility.
   701  var HopHeaders = []string{
   702  	"Connection",
   703  	"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
   704  	"Keep-Alive",
   705  	"Proxy-Authenticate",
   706  	"Proxy-Authorization",
   707  	"Te",      // canonicalized version of "TE"
   708  	"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
   709  	"Transfer-Encoding",
   710  	"Upgrade",
   711  }