-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
net/http: new HTTP client package #23707
Comments
This is a pattern in a test client I wrote:
Are you looking at adding new identifiers?
|
There would be new API for this, yes. I don't have a concrete proposal yet. |
We wrote a wrapper for the http client internally that uses the https://github.com/AlphaFlow/gohttp GET with parameters:
POST with body:
Mocking the HTTP request:
This has greatly simplified our error handling/json marshaling. Quirks
|
I've started working on this. See:
I'll move a lot of that to golang.org/x/exp/http* at some point here. |
Any problems with or changes to tracing to consider? Is https://golang.org/pkg/net/http/httptrace/ still the desired interface for tracing if there's a chance for change? |
@danp, thanks. I'll include a section about that. It's mostly okay as-is AFAIK and should ideally continue to work but if you have issues with it, do share. |
Maybe that would be a good opportunity to fix one thing that really bugs me. Right now if request fails due to context being cancelled, you get And in general, it's weird that |
Just a while ago I made a small facade w/ sane defaults whose varargs design may be of interest: https://github.com/function61/gokit/tree/master/ezhttp edit: my idea seems to be quite similar to what @awreece already posted Example:
More examples in test file Mutators my current
|
I feel like Context as a mechanism for cancelling, and Context as a way to pass data along with a request, probably ought to be different things, not the same object/parameter. I'm not sure whether that's what is meant by noting that Request.WithContext is slow? |
@lpar, redesigning context is out of scope for this bug. In practice, you end up always needing both in big systems (for tracing), and passing two context-like things around everywhere is even grosser than passing one. So one it is. That doesn't affect the performance. WithContext is slow because of the defensive garbage it makes. If you could run a request with a context to begin with (Go 1 didn't accept one), then we wouldn't need WithContext. |
While I see some examples for using options when sending the request, I often find that in my programs I want to configure those once at startup directly on the client. Thats why we wrote https://github.com/classmarkets/cmhttp. Example: client := cmhttp.Decorate(
http.DefaultClient,
cmhttp.Scoped("https://api.example.com"),
cmhttp.Typed("application/json"), // or cmhttp.JSON()
)
req, err := http.NewRequest("GET", "/v1/places/de.berlin", nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req) Like the examples of @awreece and @joonas-fi this uses varargs to let the user pick the options they want but instead of doing this on a per request basis this decorates the client. Just thought I might share that here for more inspiration for your redesign :) |
Are we open to consider respecting proxy env by default? |
Seems reasonable. |
The magic-method interfaces ( Ideally we should not only fold in the existing magic-method interfaces, but also have a plan to avoid the need for them going forward. (Perhaps use structs of functions instead?) |
I share many points enumerated in the Problems document and would like to drill a bit more into the point Client vs. Transport distinction: The current separation between Request and Client regularly leads to modifying per-request scope properties (e.g. query parameters, headers, body, ...) (using a builder pattern with late/lazy error handling would be nice) but also modifying client scope properties (e.g. base url for single host API endpoints, user-agent, timeouts, outgoing request and incoming response logging, request id or tracing span tracking, API token and refresh logic, retry logic, ...). All of these client modifications have to be done in a nested way, similar to a server's middleware chaining, using the Transport while being careful to not mix Transport struct and RoundTripper interface. Is the idea of the Handler to solve these client modifications or maybe even combining request and client modifications? As a real-world example (Experience Reports?), we use the following pattern for client/Transport middleware: func NewClient(logger *log.Logger, opts ...TransportOption) *http.Client {
c := DefaultClient()
c.Transport = NewTransport(opts...)
c.Transport = NewLoggingRoundTripper(c.Transport, logger)
c.Transport = NewSpanIDRoundTripper(c.Transport)
return c
}
// DefaultClient returns a new http.Client with similar default values to
// http.Client, but with a non-shared Transport, idle connections disabled, and
// keepalives disabled.
func DefaultClient() *http.Client {
return &http.Client{
Transport: DefaultTransport(),
}
} And the following pattern for Request building: func NewClient(httpClient *http.Client, opts ...RequestOption) *Client {
return &Client{
HTTPClient: httpClient,
RequestOpts: opts,
}
}
func (c *Client) WithOptions(opts ...RequestOption) *Client {
c.RequestOpts = append(c.RequestOpts, opts...)
return c
}
func (c *Client) WithContext(ctx context.Context) *Client {
c.RequestOpts = append(c.RequestOpts, WithContext(ctx))
return c
}
func (c *Client) WithBaseURL(u string) *Client {
c.RequestOpts = append(c.RequestOpts, WithBaseURL(u))
return c
}
func (c *Client) WithHeader(header, value string) *Client {
c.RequestOpts = append(c.RequestOpts, WithHeader(header, value))
return c
}
func (c *Client) Do(req *Request, opts ...RequestOption) (*http.Response, error) {
if c.err != nil { // abort if already in error state
return nil, c.err
}
oo := c.RequestOpts
oo = append(oo, req.Opts...)
oo = append(oo, opts...)
for _, o := range oo {
if req.err != nil { // abort if already in error state
return nil, req.err
}
o(req)
}
if req.err != nil { // abort if already in error state
return nil, req.err
}
return c.HTTPClient.Do(req.Request)
} |
We also use a lot the customisation of Transport much like @djui. We also use this to implement caching and client instrumentation for example. I do agree that better control over single request is needed, especially for timeout but I think having a way to configure client(I am not necessarily attached to RoundTripper though) should be kept in the final design. |
@bcmills, I agree, but this is a bug about the HTTP client, not server. I've pushed back on optional interfaces for some time now in all packages, though, and don't plan on using them here. |
Dear @bradfitz my wishlist:
I know some of them might not be httpclient related, but if you can fix or enable some of them while changing httpclient would be really nice. |
A feature of Python's requests and some Ruby http clients I have seen is to set up authentication once and specify a base URL to combine with paths (e.g. for different host endpoints for stage vs. prod). I don't think these should be direct features of this library, but it should be designed so that it's easy to wrap the client with something modifies the requests before sending them. With that, a lot of API client libraries out there could be even thinner wrappers than they are now, and just provide a set of JSON structs and a function that takes authentication keys and returns a working httpclient. |
What I don't understand from http client is why each request must have full URL with domain name? Why not,
? |
@shuLhan Your example seems to hint at a single-host client, which I would consider a specialization, to which optimizations can be done, especially regarding connection pooling. The provided HTTP should be generic and stateless between requests, imho. |
@djui true, but it could also be working on the return value of |
Difficulty with magic methods and lack of h2c are big thorns with the existing implementation. Cleaner interfaces and support for server handler middleware would also be good. |
Please provide a way to write to the request body. One example of where this is useful is writing a multipart request body using large files to stored on disk. It's possible to get a writer on the request body using io.Pipe and a goroutine, but it would be nice of the client handled these details or if the client plumbed a writer more directly to the network. Here's a proposed API for the bradfitz/exp-httpclient:
|
better multipart support out of the box |
Please forgive my ignorance, but is not that what a client should do? Unless we are writing a browser, I can't think of any use case where one client can or should handle connection to several hosts (except redirection). From my point of view, which I am not familiar with the underlying HTTP client code, there are two layer of abstraction that the current HTTP client trying to handle: client layer and connection pooling/management layer; and IMO they should be separated. Case in example: in database, there is an API to create single client connection and there is an API to create connection pooling. Which one user want, its depends on the use case.
generic. There are eight HTTP methods since RFC7231 (without PATCH) and I don't think it will be changed in the next ten years. The only thinks that dynamic in client is request query parameter, headers, and body; but, as bradfitz already mentioned, the Request object contains both fields required by client and server which cost allocated struct size when working only on client. stateless between requests. I am not sure what does that means, since HTTP is build on top of TCP and current HTTP client is synchronous. |
@shuLhan I guess I was thinking of other use cases such as scrapers or simpler programs/scripts. |
Not sure if the httpclient is the right way to address this, but if you just spawn a bunch of goroutines to do a bunch of HTTP requests, you can easily eat through your file descriptor limit and crash your machine. Would a concurrency limit be appropriate in a the new httpclient? If not, how could the client make it easy to add a semaphore wrapper? |
@carlmjohnson isn't existing http client already gives you all the necessary dials? You can limit keep-alive and concurrent connections to prevent file descriptor exhaustion. |
You can set DefaultTransport.MaxConnsPerHost, but AFAIK, there's no way to set a limit if you're scraping multiple hosts. I could be wrong. |
@shuLhan |
I just had another idea for the http client usage.
Maybe handlers to help pipelining large response bodies, that would be efficient in memory handling. Think of a list of Pods and you need only a fraction of data, in your data collection to process in your program. We could easily enable users to use json Encode/Decode, and not read the full Body and use Marshal/Unmarshal. Error handling would need to be done with returning
|
One more thing, I realized today: |
@bradfitz any update or timeline on this? |
Please don't. Reasons:
I think one should avoid making assumptions that are very dependent on applications and I also don't think it would be so much of a boiler plate, if needed. It feels like it sometimes might be an error and might make an if condition easier, but in others be just bothersome. While I can understand that small projects, sample code (which often skips error handling anyways though) it's simple I think in bigger projects where you are likely to wrap code anyways (to change things like user agent, etc.) it's easy enough to add one simple if condition and not check which HTTP status code is an error now. There's various scenarios where the status code is the main thing you check for, so it's convenient to know right away whether something on the transport side (and related) hasn't failed, vs. having to get the information by querying the error. Since it's related, esp. to 3xx: Any thoughts on redirects? Should they be handled? If so, how many? Should any data from redirects pages still be made available? Should caching, ETags, CookieJar, etc. be handled per default? |
[Unrelated thought]: What about typing the status codes? Currently they are just type Something like: type Status int
const StatusContinue Status = 100 Function signatures would be more expressive:
|
Brad Fitz's experimental new client package had typed status codes. It was a good idea. But I think this issue is dead now. If it ever comes back to life, probably someone will need to have a multipart strategy to make a new external API first using the existing internals, then rewire the old API to use the new one under the hood. It would be a lot of work. |
Tracking bug for higher-level more usable HTTP client functionality.
I thought we had a bug for this, but maybe not.
The text was updated successfully, but these errors were encountered: