Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] htransport: option for customizing transport #1962

Closed
broady opened this issue Apr 29, 2020 · 12 comments
Closed

[RFC] htransport: option for customizing transport #1962

broady opened this issue Apr 29, 2020 · 12 comments
Assignees
Labels
type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@broady
Copy link
Contributor

broady commented Apr 29, 2020

Allow generic tweaking of net/http:

package storage // cloud.google.com/go/storage

import raw "google.golang.org/api/storage/v1"
import htransport "google.golang.org/api/transport/http"

func NewClient(...) ... {
     _, _ = raw.NewService(ctx, htransport.WithConfigureTransport(func(t *http.Transport) {
          t.MaxIdleConns = 100
     })

     // ... or maybe:

     _, _ := raw.NewService(ctx, htransport.WithBaseTransport(&http.Transport{
          MaxIdleConns: 100,
          // but what about the defaults?!
     })
}

Ideas?

@broady broady added the triage me I really want to be triaged. label Apr 29, 2020
@broady
Copy link
Contributor Author

broady commented Apr 29, 2020

I think either could be a candidate to live long-term in google.golang.org/api/option, likely preferred and easier to use over WithHTTPClient in most cases.

It's unclear whether the parameter to WithBaseTransport should be *http.Transport or the RoundTripper interface.

@tritone
Copy link
Contributor

tritone commented Apr 29, 2020

+1 for this. In storage, for example, adding a simple RoundTripper to a client (for example adding logging or a request header) via WithHTTPClient requires duplicating a bunch of code from the NewClient and understanding how to call into htransport. Lots of potential pitfalls.

@codyoss codyoss added type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. and removed triage me I really want to be triaged. labels Apr 29, 2020
@codyoss
Copy link
Member

codyoss commented Apr 29, 2020

I would vote for using RoundTripper. Would provide a nice way for people to mock out our client requests should they choose to do so.

@broady
Copy link
Contributor Author

broady commented Apr 30, 2020

RoundTripper seems like a better fit, I agree, but it's very awkward, for example, to set just a proxy, like in CL 55030/#434 if we don't know that we're getting an actual *http.Transport.

Functionally, accepting a RoundTripper is not too far different off from http.Client, except that it's easier to wrap with HTTP header-level things (e.g., setting user-agent), but completely fails at not disturbing TCP-level options in *http.Transport. Users would have to look up what we set as MaxIdleConns (and maybe confuse that with net/http) when all they were trying to do was add a proxy, or tweak one of those knobs, etc etc.

@codyoss
Copy link
Member

codyoss commented Apr 30, 2020

How much benefit would adding one of these options have over just using https://pkg.go.dev/google.golang.org/api@v0.22.0/transport/http?tab=doc#NewTransport with option WithHTTPClient? Is there are particular use case you were thinking adding this option would solve or are you more worried about the sprawl of Transport level options we maintain?

@tritone
Copy link
Contributor

tritone commented May 4, 2020

I have run into a few issues where GCS customers want to add simple roundtrippers (e.g. add a header or logging) or change the settings for the transport, and it ends up requiring quite a bit of additional code including copying logic from NewClient in the generated client. For example here's a sample I made for adding debug headers:

func main() {
 
	ctx := context.Background()
 
	// Standard way to initialize client:
	// client, err := storage.NewClient(ctx)
	// if err != nil {
	// 	// handle error
	// }
 
	// Instead, create a custom http.Client.
	base := http.DefaultTransport
	trans, err := htransport.NewTransport(ctx, base, option.WithScopes(raw.DevstorageFullControlScope),
		option.WithUserAgent("custom-user-agent"))
	if err != nil {
		// Handle error.
	}
	c := http.Client{Transport:trans}
 
	// Add RoundTripper to the created HTTP client.
	c.Transport = withDebugHeader{c.Transport}
 
	// Supply this client to storage.NewClient
	client, err := storage.NewClient(ctx, option.WithHTTPClient(&c))
	if err != nil {
		// Handle error.
	}
}
 
type withDebugHeader struct {
	rt http.RoundTripper
}
 
func (wdh withDebugHeader) RoundTrip(r *http.Request) (*http.Response, error) {
	r.Header.Add("X-Return-Encrypted-Headers", "all")
	resp, err := wdh.rt.RoundTrip(r)
	if err == nil {
		log.Printf("Resp Header: %+v, ", resp.Header.Get("X-Encrypted-Debug-Headers"))
	} else {
		log.Printf("Error: %+v", err)
	}
	return resp, err
}

Understanding what an appropriate base client to use (including adding auth logic) is quite tricky and requires more understanding than most users have of the code in google-api-go-client. Also, this doesn't play nicely with options such as WithEndpoint and WithClientCertSource (users need to understand that they need to set these things in the http.client rather than using the options). And if we change any additional logic in NewClient at all, they are unable to pick that up.

This PR I made (https://code-review.googlesource.com/c/gocloud/+/55411 ) also shows the difficulty that this poses for making changes to the transport at the level of manual clients.

@ernestas2k
Copy link

ernestas2k commented May 11, 2020

+1 at least to provide additional RoundTripper. In our case for request/response logging. If any assistance is needed, let me know.

@broady
Copy link
Contributor Author

broady commented Jun 16, 2020

If we consider the different layers of what our "transport" package does, then we can consider where we might allow people to customize.

Too many options that operate at the same level might indicate redundancy and that a generic option might be more appropriate.

HTTP GRPC
WithTokenSource/WithAPIKey/WithQuotaProject/WithRequestReason/WithUserAgent ditto
??? - proposed http.RoundTripper/http.Transport/http.Proxy ??? grpc.ClientConnInterface
WithHTTPClient WithGRPCConn WithGRPCDialOption
??? - proposed WithDialer(func (context.Context, string) (netConn, error)) WithGRPCDialOption(grpc.WithDialer(...))
WithEndpoint ditto

Options that work the same across both transports are desirable and might indicate an appropriate abstraction.

FWIW I think WithHTTPClient and one of WithGRPCConn or WithGRPCDialOption were probably mistakes.

ericnorris added a commit to ericnorris/terraform-provider-google that referenced this issue Jul 29, 2021
This commit partially addresses hashicorp#9454, in the case when the provider is
configured with both the user_project_override AND billing_project
setting, by setting a header in the LoadAndValidate function stage on
the HTTP client used by all handwritten resources.

Originally this commit used a Google API client ClientOption [1],
but unfortunately headers set by the Google API client's transport
implementation [2] aren't visible by the logging transport [3]. I
thought it would be better to include the header at a higher level in
order to aid with debugging in the future. If [4] is ever merged, moving
the logging transport _inside_ of the API client and switching to a
ClientOption for the header would probably be a better way.

Finally, this does not address having user_project_override set to true
without the billing_project setting, as that would require more logic to
determine which project the resource is referring to.

[1] https://pkg.go.dev/google.golang.org/api/option#WithQuotaProject
[2] https://github.com/googleapis/google-api-go-client/blob/3e2b6a25f224e301409d11443d464af92671d2f0/transport/http/dial.go#L86-L88
[3] https://github.com/hashicorp/terraform-provider-google/blob/0e315b07d9ed37bd884a1060c22681908d23f270/google/config.go#L373-L374
[4] googleapis/google-cloud-go#1962
@tritone
Copy link
Contributor

tritone commented Nov 4, 2021

Over time we've done some additional customization of the HTTP client for optimization (using http2 configs, MaxIdleConnsPerHost, etc). Just talked to another user who was attempting to add a roundtripper to do simple header manipulation but, using option.WithHTTPClient, missed out on these settings unwittingly. I think it would be good to move ahead with this when we have a chance.

@codyoss
Copy link
Member

codyoss commented Nov 4, 2021

I will add this a point of discussion in our next meeting. I would like to formally capture use-cases we are not supporting well today.

@tkinz27
Copy link

tkinz27 commented Mar 11, 2022

To add a use case, I am in the process of trying to lock the GCS storage client to a specific local storage interface. My application runs on an embedded linux server that has access to LTE and WiFi modems, and we want to guarantee that our file uploads/downloads are locked to the wifi interface.

Having an equivalent to grpc.WithContextDialer would be much much appreciated. Right now I have done my best to follow how the storage client creates a http client and am using option.WithHTTPClient.

@codyoss
Copy link
Member

codyoss commented Apr 16, 2024

As of right now we will not be moving forward with this. In our new auth module we tried to make adding auth to an existing http.Client a bit easier. Using that and the WithHTTPClient Option is how we recommend customising a transport.

@codyoss codyoss closed this as completed Apr 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
Development

No branches or pull requests

5 participants