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

Use the Client interface to represent an HTTP client #70

Closed
wants to merge 1 commit into from
Closed

Use the Client interface to represent an HTTP client #70

wants to merge 1 commit into from

Conversation

francescomari
Copy link

Instead of using *http.Client directly, it would be nice if requests used an interface to represent an HTTP client. This allows the user to plug different implementation of the client that implement cross-cutting concerns like retries.

More specifically for me, I would love to use this project to simplify some of my code, but the projects I'm working on already define and pass around interfaces like this to represent an HTTP client:

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

Given this prerequisites, it's impossible for me to use this library without type-casting and failing at runtime. Instead, if this PR was merged, integrating with requests would be a piece of cake.

@earthboundkid
Copy link
Owner

Github is having problems today, so it's eating my comments.

@earthboundkid
Copy link
Owner

Interesting. It feels sort of redundant with Transport/RoundTripper, but that’s a problem with the standard library (if there’s ever a standard library v2, it should turn http.Client into just a middleware RoundTripper that adds redirect following etc onto a base RoundTripper instead of having two basically identical interfaces with overlapping concerns).

I don't like having setting the Transport fail silently. It should explicitly refuse to connect if given contradictory instructions. The question is whether it should fail by returning an error or panicking. Panicking is more “correct” because setting contradictory options is a programmatic failure, not a runtime failure, but it’s also pretty gross, and I don’t like it.

I think the simpler option that would let us sidestep the question of failing transports is to have a function like this:

// Doer is an interface with a Do method. 
// It exists for compatibility with other libraries.
// Users should prefer Transport, 
// because Do is a wrapper around http.Client 
// which has higher level concerns.
type Doer interface {
	Do(req *http.Request) (*http.Response, error)
}


// DoerTransport converts a Doer into a Transport (http.RoundTripper).
func DoerTransport(cl Doer) Transport {
  return RoundTripFunc(func (req *http.Request) (*http.Response, error) {
    return cl.Do(req)
  })
}

Is there some reason this wouldn’t work with your library?

@francescomari
Copy link
Author

The problem with using RoundTripper is that it works at a different level of abstraction than http.Client.

Quoting from the documentation, "RoundTrip executes a single HTTP transaction" (and is therefore the wrong place to plug concerns like retries), and "RoundTrip should not attempt to handle higher-level protocol details such as redirects, authentication, or cookies" (which are all tasks that are anyway already handled via the requests library, because the library acts as an HTTP client with extra features).

Moreover, I should have read the contributing guidelines and started a conversation first, apologies. I just noticed that #56 would be a good match.

I don't like having setting the Transport fail silently. It should explicitly refuse to connect if given contradictory instructions. The question is whether it should fail by returning an error or panicking. Panicking is more “correct” because setting contradictory options is a programmatic failure, not a runtime failure, but it’s also pretty gross, and I don’t like it.

I think there are no good options at this point. I'm in favour for panicking, since setting both a RoundTripper and a custom client that is not an http.Client is a programmer error. But I don't think this will happen often. Why would a user want to plug an HTTP client and a RoundTripper together? In the project mentioned above, we rolled logging, observability and retries in one standard client used all over the place, which makes using RoundTrippers obsolete. The RoundTripper is part of our "standard" HTTP client configuration (indirectly, via go-cleanhttp), and it's a low-level detail I don't usually need to mess with.

@earthboundkid
Copy link
Owner

I think the docs are a little muddled and out of date. If you read the code for http.Client.Do, all it does is handle retries and apply a CookieJar. It really should just be a middleware RoundTripper, but the Go team is stuck with their old architecture because of the Go 1 guarantee. There are several libraries that handle retries as an http.RoundTripper, such as https://github.com/ybbus/httpretry#modify--customize-the-roundtripper-httptransport, and under the hood, the Google OAuth RoundTripper issues multiple requests (because that's how OAuth works).

@francescomari
Copy link
Author

These are all good examples in favour of RoundTrippers as substitute, higher-level HTTP clients. I think your proposal of converting a Doer to a RoundTripper would work in my project, even if it's not as nice as having a generic .Client() method on the builder.

earthboundkid added a commit that referenced this pull request Mar 27, 2023
@earthboundkid
Copy link
Owner

earthboundkid commented Mar 27, 2023

Take a look at v0.23.3-alpha1 and see if it works for your usecase.

@francescomari
Copy link
Author

It looks good! Thanks a lot for taking care of this, it will make using this library so much easier for me. I will close this PR, as the change is already in your own.

earthboundkid added a commit that referenced this pull request Mar 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants