-
Notifications
You must be signed in to change notification settings - Fork 15
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
Add context support, ErrUnauthorized and Doer interface for http.Client #4
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for contributing.
Aside from the fmt.Println(resp.StatusCode, string(b))
change, which looks unintended, all other code changes look good.
However, I see two main issues/potential improvements for this PR.
- All code changes are in a single commit. This is harder to review, and not a good practice. Would you mind splitting each logical change like "add context support, "add Doer interface", "add http post form", etc., into a separate commit?
- No documentation. I know a lot of the code here isn't perfect, but let's try to make things better. Exported symbols need to be documented. This is a good practice and consistent with Go style. Try running
golint
on your changes.
Aside from those 2 things, these changes look great and make sense to me. I'd be happy to accept them. I'm guessing @tambet would like them too.
Are you willing to address the two suggestions I've made above?
fmt.Println("---------------------------") | ||
fmt.Println(resp.StatusCode, string(b)) | ||
fmt.Println("---------------------------") | ||
err = json.NewDecoder(bytes.NewBuffer(b)).Decode(res) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you forget to remove this change after testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm so sorry for this debugging printing
I will add documentation and split commits at this weekend.
Thank you for your feedback!
Tried to split commits as much as possible, add documentation, also add tests this time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great. I've left minor comments and questions in my review. Thanks!
|
||
//DoerFunc implements Doer interface. | ||
//Allow to transform any appropriate function "f" to Doer instance: DoerFunc(f). | ||
DoerFunc func(req *http.Request) (resp *http.Response, err error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, and elsewhere, the comments don't have a space after the two slashes. There should be a single space there.
See here for rationale.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix this, thank you for explanations.
//Use it as point of setting Auth header or custom status code error handling. | ||
Doer interface { | ||
Do(req *http.Request) (*http.Response, error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a guess, but could you please elaborate on why you decided to make the Doer
interface instead of sticking with an *http.Client
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It allows me to use single instance of my configured http.Client and set different Auth headers on each request.
I have configured http.Client and it is property of my own Client struct.
type Client struct {
httpClient *http.Client
}
My service receiving auth token as an argument to request and with Doer interface it is easy to add Auth header with token to request like this:
func (c *Client) withToken(token string) *asana.Client {
f := func(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+token)
return c.httpClient.Do(req)
}
return asana.NewClient(asana.DoerFunc(f))
}
// Usage example
func (c *Client) Issue(ctx context.Context, token string, issueID int64) (*Issue, error) {
t, err := c.withToken(token).GetTask(ctx, issueID, issueFieldsFilter)
if err != nil {
return nil, err
}
issue := toIssue(t)
return &issue, nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation.
This sounds reasonable, but I also slightly worry this might not be the most optimal solution for the problem you're trying to solve. Having Doer
be an interface lets you pass in a middleware-augmented *http.Client
of sorts, but you still have to create a new asana.NewClient
for each one...
It's also interesting that go-github, a very popular library, manages to get by without Doer
interface. /cc @willnorris, do you know how one would do that with go-github?
I'm not opposed to this going in @anpryl, I just wanted to say that it seems a little more questionable to me, and perhaps we can find a better way to effectively be able to augment certain *http.Request
s. But maybe not, and this is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this client fully taking on responsibility for authentication, or only for certain calls, like in your c.Issue method? go-github's Client knows nothing about authentication at all. It is always punted to the underlying http.Client, which typically comes from golang.org/x/oauth2, or the basic auth client that go-github does provide (but is otherwise completely separated from the Client struct).
I think the best analogy to this in go-github would be the custom accept headers we set on various API calls. That happens by first building the http.Request, then adding whatever custom headers we want before passing it off to client.Do. As long as the underlying http.Client leaves those headers intact, then you're fine. If your underlying http.Client is clobbering your auth header because it thinks it should be responsible for auth, then you do have a problem.
This is exactly why go-github's Client struct has its own Do and NewRequest methods, so that you can modify the request before calling Do. Not separating those, as go-asana does with its single Request method, results in these weird workarounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation @willnorris, that makes sense.
I agree that the Do
and NewRequest
separation makes more sense and is cleaner/safer, but I'd want to think more and understand it better (outside of this PR). I think we should consider changing this library in that direction, rather than the Doer
, which I think is a lot harder for people to use (at all, and also correctly).
What are your thoughts on this @anpryl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need Doer
to reuse http.Client
with different tokens.
But this change will decrease cost of NewClient
function call.
We could also include withToken
function into package and make it public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, okay. It's been a while and I have no better ideas than Doer
interface, so let's go with that for now. If really good ideas come in the future, we can probably still change things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can I move url.URL
to global variable and initialize it in init()
funciton?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to split up orthogonal changes across multiple PRs and keep each one smaller.
Mind making it a separate followup PR? This one is already huge. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, you are right, I will prepare new PR after merge.
} | ||
|
||
Error struct { | ||
Phrase string `json:"phrase,omitempty"` | ||
Message string `json:"message,omitempty"` | ||
} | ||
|
||
Errors []Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any restrictions on Errors
? Can it contain 0 elements? 1 element?
@@ -272,7 +283,7 @@ func (c *Client) request(method string, path string, data interface{}, opt *Filt | |||
res := &Response{Data: v} | |||
err = json.NewDecoder(resp.Body).Decode(res) | |||
if len(res.Errors) > 0 { | |||
return res.Errors[0] | |||
return res.Errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to this, at least 1 element. I think it's worth documenting that for the Errors
type, so users will know to always expect either at least one error there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct, add godoc for Errors type
@@ -14,6 +15,7 @@ var ( | |||
client *Client | |||
mux *http.ServeMux | |||
server *httptest.Server | |||
ctx = context.Background() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's a test, but still, this isn't saving much typing. Instead of this variable here, just use context.Background()
directly at the call sites. E.g.:
workspaces, err := client.ListWorkspaces(context.Background())
...
users, err := client.ListUsers(context.Background(), nil)
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to write 3 letters instead of context.Background() in tests, but it's not a big deal. I remove this global variable and put context.Background().
} | ||
|
||
// request makes a request to Asana API, using method, at path, sending data with opt filter. | ||
// The response is populated into v, and any error is returned. | ||
func (c *Client) request(ctx context.Context, method string, path string, data interface{}, opt *Filter, v interface{}) error { | ||
func (c *Client) request(ctx context.Context, method string, path string, data interface{}, form url.Values, opt *Filter, v interface{}) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now request
has 2 ways of specifying the data. Either by setting data
, or by setting form
. I think it might be helpful to document that at most one can be used at the same time (or none), and that data
takes higher precedence.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve request documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//Use it as point of setting Auth header or custom status code error handling. | ||
Doer interface { | ||
Do(req *http.Request) (*http.Response, error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation.
This sounds reasonable, but I also slightly worry this might not be the most optimal solution for the problem you're trying to solve. Having Doer
be an interface lets you pass in a middleware-augmented *http.Client
of sorts, but you still have to create a new asana.NewClient
for each one...
It's also interesting that go-github, a very popular library, manages to get by without Doer
interface. /cc @willnorris, do you know how one would do that with go-github?
I'm not opposed to this going in @anpryl, I just wanted to say that it seems a little more questionable to me, and perhaps we can find a better way to effectively be able to augment certain *http.Request
s. But maybe not, and this is fine.
This is more consistent with the rest of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. See #4 (comment).
I'll let this sit for a day and merge tomorrow if no further comments. /cc @tambet
Thanks for your work @anpryl!
Merged. This is a breaking API change, but it includes some necessary API improvements such as adding support for passing a context. Updating should be easy, and those who don't want to will have this library vendored anyway. Thanks for your awesome contribution @anpryl! |
Extend API for context usage.
Make Errors an error instance.