Skip to content

In CreateFork, return the repo info when 202 Accepted occurs.#878

Merged
gmlewis merged 1 commit intogoogle:masterfrom
dsymonds:master
Apr 3, 2018
Merged

In CreateFork, return the repo info when 202 Accepted occurs.#878
gmlewis merged 1 commit intogoogle:masterfrom
dsymonds:master

Conversation

@dsymonds
Copy link
Copy Markdown
Contributor

Per the docs
(https://developer.github.com/v3/repos/forks/#create-a-fork), there is
still a useful body, and it is helpful for automatic fork creation to
get that response.

I'm not sure if this approach is okay. An alternate is to do the decoding inside Do, so this works for the ~dozen other places that might get an AcceptedError, but I didn't want to go check the docs for every single case to see whether the response body is meant to have useful data.

@googlebot googlebot added the cla: yes Indication that the PR author has signed a Google Contributor License Agreement. label Mar 26, 2018
@dmitshur
Copy link
Copy Markdown
Member

dmitshur commented Mar 26, 2018

This is related to #862. We can use information there when reviewing this.

(Also somewhat relevant are #486, #488 where AcceptedError was initially designed and implemented.)

@dsymonds
Copy link
Copy Markdown
Contributor Author

dsymonds commented Apr 2, 2018

I'm not clear what's happening, or who this is blocked on. #862 hasn't had any activity in a month.

@dsymonds
Copy link
Copy Markdown
Contributor Author

dsymonds commented Apr 2, 2018

cc @gmlewis

@gmlewis
Copy link
Copy Markdown
Collaborator

gmlewis commented Apr 2, 2018

Sorry I missed this. Ideally, we would like to do something similar to this for all endpoints that potentially return 202, but my opinion is to go ahead and move forward with this, since there is an immediate need.
Then, if it becomes clear that more endpoints need processing like this, we can address that then.

Copy link
Copy Markdown
Collaborator

@gmlewis gmlewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks, @dsymonds!

@gmlewis
Copy link
Copy Markdown
Collaborator

gmlewis commented Apr 2, 2018

Awaiting second LGTM before merging.

@gmlewis gmlewis requested a review from willnorris April 2, 2018 23:15
Copy link
Copy Markdown
Member

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would like to do something similar to this for all endpoints that potentially return 202, but my opinion is to go ahead and move forward with this, since there is an immediate need.
Then, if it becomes clear that more endpoints need processing like this, we can address that then.

Agreed, that's a good way to proceed.

It would be nice to add a test case, so this behavior doesn't regress (especially if we refactor this code in the future to make it more general). @dsymonds Can you add a test case?

I also left one minor style suggestion, see inline comment.

json.NewDecoder(resp.Body).Decode(fork)
return fork, resp, err
}
if err != nil {
Copy link
Copy Markdown
Member

@dmitshur dmitshur Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider merging this with the if statement added above, so it's a little easier to see that this if statement deals with the same err value from line 81:

if _, ok := err.(*AcceptedError); ok {
	...
} else if err != nil {
	...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine keeping them separate. They are very different code paths; the AcceptedError case is only nominally an error.

@dsymonds
Copy link
Copy Markdown
Contributor Author

dsymonds commented Apr 3, 2018

Good call on adding a test. It turned up a bug in Client.Do that would close the response body on error, preventing the caller from being able to process it at all.

Copy link
Copy Markdown
Member

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the test case, it looks great.

github/github.go Outdated
// function, read its contents now so the caller can use it,
// and wrap it such that it doesn't matter if they close it.
body, _ := ioutil.ReadAll(response.Body)
response.Body = ioutil.NopCloser(bytes.NewReader(body))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this makes sense, but I don't think it's the right place.

We don't want it to run in all cases when err != nil. If you look at the code of CheckResponse, it does the following things:

  1. if status code is accepted, it immediately returns &AcceptedError{}
  2. if status code is in 2xx range, it returns nil
  3. it reads the response body into a byte slice (data, err := ioutil.ReadAll(r.Body))
  4. it returns some specific error type or a general &ErrorResponse{Response: r}

We don't want this code to run after ioutil.ReadAll(r.Body) already happened in CheckResponse.

I think a good fix is to move this code into CheckResponse's first case:

func CheckResponse(r *http.Response) error {
	if r.StatusCode == http.StatusAccepted {
		// Since Do closes the original response body upon returning,
		// read its contents now so the caller can use it,
		// and wrap it such that it doesn't matter if they close it.
		body, _ := ioutil.ReadAll(r.Body)
		r.Body = ioutil.NopCloser(bytes.NewReader(body))
		return &AcceptedError{}
	}
	...
}

Copy link
Copy Markdown
Member

@dmitshur dmitshur Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives me a followup idea. Instead of using r.Body as a way of transmitting the body from here into CreateFork, we can put it inside AcceptedError:

type AcceptedError struct {
	body []byte
}

func CheckResponse(r *http.Response) error {
	if r.StatusCode == http.StatusAccepted {
		// Since Do closes the original response body upon returning,
		// read its contents now so that CreateFork can use it.
		body, _ := ioutil.ReadAll(r.Body)
		return &AcceptedError{body: body}
	}
	...
}

Then CreateFork code becomes more clear:

 	fork := new(Repository)
 	resp, err := s.client.Do(ctx, req, fork)
+	if ae, ok := err.(*AcceptedError); ok {
+		json.NewDecoder(bytes.NewReader(ae.body)).Decode(fork)
+		return fork, resp, err
+	}
 	if err != nil {

I think this has the benefits of not changing externally visible behavior of the library (edit: actually, it still changes CheckResponse slightly), and it's easier to read and be sure is correct. If resp.Body is modified as currently done, then all other endpoints which don't specifically handle AcceptedError will be returning a resp with a body. Users may start to rely on that, and we don't neccessary want to promise it'll always work that way.

In the future, we can consider either exporting AcceptedError.body so users of this package can use it, or implement a different solution if we find a better one.

What do you think about this idea?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing that in CheckResponse merely hides the issue from the non-trivial control flow in Do (which is where the body closing is happening). Right now, CheckResponse doesn't touch the body in the HTTP 202 case, just like in any of the other 2xx cases. Your suggestion would mean that Do needs to know that CheckResponse does that transformation and therefore closing the body is okay.

Now Do currently handles all the non-error decoding, so perhaps it should handle this case too. It has the destination object there already. Then the body closing is moot. Let me take that approach; I think it may be the best option.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In regard to your follow-up idea: I think that's unnecessary. The AcceptedError flows are generally all places where the response body is some subset of the regular response (that's why it is a 2xx code rather than a different code), so forcing the callers to do the parsing seems like unnecessary work. I think my latest revision (having Do do the decoding when there's an AcceptedError) is the simplest option, and it still preserves existing code behaviour unless the caller is doing something truly odd.

@dmitshur
Copy link
Copy Markdown
Member

dmitshur commented Apr 3, 2018

One more note. When making changes to this PR, you don't have to squash and force push. Feel free to send distinct followup commits into this PR. We will squash them into a single commit when merging this PR.

GitHub PRs don't have a way of seeing history, so keeping changes as separate commits makes them easier to review.

Per the docs
(https://developer.github.com/v3/repos/forks/#create-a-fork), there is
still a useful body, and it is helpful for automatic fork creation to
get that response.
@dsymonds
Copy link
Copy Markdown
Contributor Author

dsymonds commented Apr 3, 2018

PTAL. I think this is much more straightforward now.

Copy link
Copy Markdown
Member

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’m in agreement, this is better. This way of treating AcceptedError more like nil error in Do and reusing the applicable decoding code there makes sense. There might be minor refactoring opportunities, but that can be done later.

LGTM.

Thanks @dsymonds.

@gmlewis Does it still look okay to you, given the code has changed?

@gmlewis
Copy link
Copy Markdown
Collaborator

gmlewis commented Apr 3, 2018

Very nice! Thank you, @dsymonds and @shurcooL! I like this a lot.
LGTM.
Merging.

@gmlewis gmlewis merged commit 88eb4e9 into google:master Apr 3, 2018
nbareil pushed a commit to nbareil/go-github that referenced this pull request May 1, 2018
…#878)

Per the docs
(https://developer.github.com/v3/repos/forks/#create-a-fork), there is
still a useful body, and it is helpful for automatic fork creation to
get that response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: yes Indication that the PR author has signed a Google Contributor License Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants