Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore: idea: unify http responses further #941

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

Merged
merged 13 commits into from
Apr 12, 2022
Merged

chore: idea: unify http responses further #941

merged 13 commits into from
Apr 12, 2022

Conversation

f0ssel
Copy link
Contributor

@f0ssel f0ssel commented Apr 8, 2022

I'd like to open a question to the team: Would we like to have a unified response pattern like this PR implements?

I've seen quite a few APIs, mostly from clouds, that do something similar. By doing this pattern, our clients can always expect the same format of response for success messages, errors, data, etc. I've always felt it was easier to work with as a consumer of the JSON API.

This was a discussion that led to just making httpapi.Write() useful for writing json data as well as the predefined response type.

@codecov
Copy link

codecov bot commented Apr 8, 2022

Codecov Report

Merging #941 (04db053) into main (19b4323) will decrease coverage by 4.66%.
The diff coverage is n/a.

❗ Current head 04db053 differs from pull request most recent head e234374. Consider uploading reports for the commit e234374 to get more accurate results

@@            Coverage Diff             @@
##             main     #941      +/-   ##
==========================================
- Coverage   66.22%   61.56%   -4.67%     
==========================================
  Files         240      107     -133     
  Lines       14408     1137   -13271     
  Branches      115      115              
==========================================
- Hits         9542      700    -8842     
+ Misses       3904      412    -3492     
+ Partials      962       25     -937     
Flag Coverage Δ
unittest-go-macos-latest ?
unittest-go-postgres- ?
unittest-go-ubuntu-latest ?
unittest-go-windows-2022 ?
unittest-js 61.56% <ø> (ø)
Impacted Files Coverage Δ
coderd/coderd.go
coderd/files.go
coderd/gitsshkey.go
coderd/httpapi/httpapi.go
coderd/httpmw/ratelimit.go
coderd/organizations.go
coderd/parameters.go
coderd/provisionerjobs.go
coderd/templates.go
coderd/templateversions.go
... and 123 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 19b4323...e234374. Read the comment docs.

Comment on lines 39 to 42
response := httpapi.Response{
Data: &data,
}
err = json.NewDecoder(res.Body).Decode(&response)
Copy link
Member

Choose a reason for hiding this comment

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

This works? TIL

Copy link
Contributor Author

Choose a reason for hiding this comment

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

json pkg be cray

@Emyrk
Copy link
Member

Emyrk commented Apr 8, 2022

Agree on a standard data field

Copy link
Member

@kylecarbs kylecarbs left a comment

Choose a reason for hiding this comment

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

I think httpapi.Response is our unified response pattern. Putting all message data inside a data field may promote sending a message or error along with a payload, which I'm not sure we have a use case for right now.

I'm not opposed to standardizing render, and I'd be fine with adding httpapi.Write with the third argument being an interface{} that we marshal for the caller.

By moving all data inside a data property, we wouldn't standardize the payload that our API consumers expect - we move the problem to the data field. API consumers can use the status code to check if the data is truly what we say it is.

Errors []Error `json:"errors,omitempty" validate:"required"`
Message string `json:"message,omitempty"`
Errors []Error `json:"errors,omitempty"`
Data interface{} `json:"data,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

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

Do you envision sending data with a message?

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 8, 2022

I also know that returning a top level json array is a security issue - http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ , and this would make sure that the caller never has to know about or consider this risk.

@kylecarbs
Copy link
Member

That article appears to call out security vulnerabilities that happened in 2009, but I'd be hesitant to say anything exists like this today. GitHub, Google, GitLab and more return top-level JSON arrays and have for years: https://docs.github.com/en/rest/reference/repos

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 8, 2022

How is httpapi.Response a unified pattern if most routes (anything that sends data) not use it? It seems we have a cognitive split when returning responses by using two different packages.

If we want to continue to just return data types directly I think we need to rename httpapi.Response to be more specific to errors since that's really its only value.

If we value status codes, I think we should not be returning 200s with messages like "I did the thing" because it's useless info. We should just send back an empty 200 or the updated data types.

@kylecarbs
Copy link
Member

Every route does use it, just depends on the status code being responded. Every error gets httpapi.Response returned right now.

httpapi.Response is intentionally not named error, because the status code represents whether it was an error or not. We can remove the message that acknowledges a success message, I agree that's useless info. It's primarily there to avoid sending back an empty object or no payload at all, because we'd be breaking the API expectation that our responses are JSON.

@ammario
Copy link
Member

ammario commented Apr 9, 2022

I've seen quite a few APIs, mostly from clouds, that do something similar. By doing this pattern, our clients can always expect the same format of response for success messages, errors, data, etc. I've always felt it was easier to work with as a consumer of the JSON API.

@f0ssel The cloud's have enormous APIs that demand peculiar patterns. The GCP compute API comes to mind. They have to rely on patterns like the one you proposed to scale to thousands of API endpoints in a consistent way. These abstractions are necessary but I don't enjoy interacting with these APIs.

Can you provide examples of smaller, more Coder-like APIs that use something like your proposed pattern? I haven't ever worked with an API where the meat of every response was nested under a specific field. I assume most APIs avoid this nesting to save developers from one level of destructuring on every response parse.

I do see the value of standard message and errors fields. Perhaps the httpapi.Response could be genericized and extended to other response types if we want to enforce those standards while keeping the structure as flat as possible. We'd have to override MarshalJSON too but this should be simple.

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 9, 2022

I actually don't care that much about the json format, but I think the real problem I have is we just have a weird mix of "generic" functions that can only be used sometimes right now. We use httpapi.Write for error and "empty " responses, but not actual data. If httpapi had a clear public api that handled the different types of responses I'd be happy.

Maybe we just need httpapi.Write to take an interface{}, I think id be happy today with that.

@kylecarbs what about an api like:

  • httpapi.Write(wr, code int, body interface{})
  • httpapi.Response to httpapi.Error
  • we get rid of the "empty" success message responses in favor of passing nil or actually returning the updated data.

That seems like a pattern we could all follow implicitly I think.

@kylecarbs
Copy link
Member

I think httpapi.Write(rw, code, body) seems reasonable. I'm hesitant to rename httpapi.Response to httpapi.Error, because I think it's fine to be used for success messages too! It's really a generic response, and the status code indicates whether the message is an error or not.

If it's called httpapi.Error, we have two separate indicators in the payload that indicate error, which seems unnecessary. I'd be happy if you made the mass change in this PR for httpapi.Write. I think we should still use render, just so we don't have to write marshal code ourselves (even though it's simple if it works it works).

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 9, 2022

I missed your point about always sending valid JSON instead of empty responses. I'll concede that does seem important to keep standard.

@kylecarbs
Copy link
Member

You found an interesting nit where the existing httpapi.Write didn't use render. We don't have to, but I agree it should cerrrrrrtainly be the same!

@kylecarbs
Copy link
Member

Ahh up to you whether we use render or not actually. I see we'd have to pass the r object to every write, which seems a bit excessive. Feel free to copy/paste from render in that case if you'd like, or we can keep the previous write code and I still think this is a substantial improvement.

@coadler
Copy link
Contributor

coadler commented Apr 9, 2022

Can we just return http.StatusNoContent instead of response messages?

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 9, 2022

@coadler I'm game in theory but I do feel like always returning JSON can't hurt. Also I do think status codes are important. What if we added something like:

func OK() Response {
	return Response{
		Message: "OK",
	}
}

Usage:

httpapi.Write(rw, http.StatusOK, httpapi.OK())

@kylecarbs
Copy link
Member

I'd rather us just write something a lil friendly in the API. Another benefit of the message not being static is us dev-folk will never check the message content, and will always check the status code 😎

Copy link
Member

@kylecarbs kylecarbs left a comment

Choose a reason for hiding this comment

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

This is a wise and nice improvement. Can we remove render from everywhere?

@coadler
Copy link
Contributor

coadler commented Apr 9, 2022

It seems a bit wasteful to me in a SaaS context to send back a bunch of data a user will never consume, but I don't feel super strongly. I really like how Discord uses http 204, which is why I bring it up.

@kylecarbs
Copy link
Member

Ahh if Discord uses it, then that's good external precedent beyond my own judgment. I'm fine with doing that in cases where no response is needed. Sending fewer bytes is certainly the hackery thing to do 🤓!

@kylecarbs
Copy link
Member

Good catches on the responses that weren't even being checked properly (OK instead of created)! 🧹

@coadler
Copy link
Contributor

coadler commented Apr 9, 2022

I also know that returning a top level json array is a security issue - http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ , and this would make sure that the caller never has to know about or consider this risk.

Also regarding this, I typically don't like returning raw JSON arrays because it's always a breaking change if we wanted to add extra fields, such as paging info. In V1 we always wrap array responses.

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 9, 2022

I also know that returning a top level json array is a security issue - http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ , and this would make sure that the caller never has to know about or consider this risk.

Also regarding this, I typically don't like returning raw JSON arrays because it's always a breaking change if we wanted to add extra fields, such as paging info. In V1 we always wrap array responses.

This sounds like a good case for a custom lint rule for not passing arrays to httpapi.Write

@kylecarbs
Copy link
Member

I think we should make that change if the time comes. Considering many APIs haven't needed to do that (eg. GitHub), I'm weary to implement it on a hunch.

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 10, 2022

I think we should make that change if the time comes. Considering many APIs haven't needed to do that (eg. GitHub), I'm weary to implement it on a hunch.

I just checked and you are right, GitHub doesn't seem to mind sending back top level arrays. Good enough for me.

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

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

I like this.

@f0ssel
Copy link
Contributor Author

f0ssel commented Apr 11, 2022

@kylecarbs do you know what may have changed to make this happen? https://github.com/coder/coder/runs/5980847240?check_suite_focus=true

@kylecarbs
Copy link
Member

Woah... zero clue. I'd call this a flake, but I have no idea how it'd happen.

@kylecarbs
Copy link
Member

@f0ssel it's because of this: https://github.com/coder/coder/pull/941/files#diff-5499c2aac6d11c8e0574c18b1dd90a14bfb063426e07115d0e453adb08c33574R35

You probably shouldn't use our API package for this, because we can just rw.Write instead.

@f0ssel f0ssel merged commit d9d4599 into main Apr 12, 2022
@f0ssel f0ssel deleted the f0ssel/res-data branch April 12, 2022 15:17
@misskniss misskniss added this to the V2 Beta milestone May 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants