Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/userlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestUserList(t *testing.T) {

_, err := cmd.ExecuteC()

var apiErr *codersdk.Error
var apiErr *codersdk.HTTPError
require.ErrorAs(t, err, &apiErr)
require.Contains(t, err.Error(), "Try logging in using 'coder login <url>'.")
})
Expand Down
4 changes: 2 additions & 2 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func New(options *Options) *API {

r.Route("/api/v2", func(r chi.Router) {
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
Message: "Route not found.",
})
})
Expand All @@ -144,7 +144,7 @@ func New(options *Options) *API {
debugLogRequest(api.Logger),
)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(w, http.StatusOK, httpapi.Response{
httpapi.Write(w, http.StatusOK, codersdk.Response{
//nolint:gocritic
Message: "👋",
})
Expand Down
2 changes: 1 addition & 1 deletion coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
}

user, err := client.CreateUser(context.Background(), req)
var apiError *codersdk.Error
var apiError *codersdk.HTTPError
// If the user already exists by username or email conflict, try again up to "retries" times.
if err != nil && retries >= 0 && xerrors.As(err, &apiError) {
if apiError.StatusCode() == http.StatusConflict {
Expand Down
3 changes: 2 additions & 1 deletion coderd/csp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"

"cdr.dev/slog"
)
Expand All @@ -22,7 +23,7 @@ func (api *API) logReportCSPViolations(rw http.ResponseWriter, r *http.Request)
err := dec.Decode(&v)
if err != nil {
api.Logger.Warn(ctx, "csp violation", slog.Error(err))
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to read body, invalid json.",
Detail: err.Error(),
})
Expand Down
10 changes: 5 additions & 5 deletions coderd/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
switch contentType {
case "application/x-tar":
default:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Unsupported content type header %q.", contentType),
})
return
Expand All @@ -41,7 +41,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(rw, r.Body, 10*(10<<20))
data, err := io.ReadAll(r.Body)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to read file from request.",
Detail: err.Error(),
})
Expand All @@ -65,7 +65,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
Data: data,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error saving file.",
Detail: err.Error(),
})
Expand All @@ -80,7 +80,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
func (api *API) fileByHash(rw http.ResponseWriter, r *http.Request) {
hash := chi.URLParam(r, "hash")
if hash == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "File hash must be provided in url.",
})
return
Expand All @@ -91,7 +91,7 @@ func (api *API) fileByHash(rw http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching file.",
Detail: err.Error(),
})
Expand Down
2 changes: 1 addition & 1 deletion coderd/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestDownload(t *testing.T) {
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, _, err := client.Download(context.Background(), "something")
var apiErr *codersdk.Error
var apiErr *codersdk.HTTPError
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
Expand Down
16 changes: 8 additions & 8 deletions coderd/gitsshkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (api *API) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) {

privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error generating a new SSH keypair.",
Detail: err.Error(),
})
Expand All @@ -35,7 +35,7 @@ func (api *API) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) {
PublicKey: publicKey,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating user's git SSH key.",
Detail: err.Error(),
})
Expand All @@ -44,7 +44,7 @@ func (api *API) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) {

newKey, err := api.Database.GetGitSSHKey(r.Context(), user.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user's git SSH key.",
Detail: err.Error(),
})
Expand All @@ -70,7 +70,7 @@ func (api *API) gitSSHKey(rw http.ResponseWriter, r *http.Request) {

gitSSHKey, err := api.Database.GetGitSSHKey(r.Context(), user.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user's SSH key.",
Detail: err.Error(),
})
Expand All @@ -90,7 +90,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
agent := httpmw.WorkspaceAgent(r)
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resource.",
Detail: err.Error(),
})
Expand All @@ -99,7 +99,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {

job, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build.",
Detail: err.Error(),
})
Expand All @@ -108,7 +108,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {

workspace, err := api.Database.GetWorkspaceByID(r.Context(), job.WorkspaceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace.",
Detail: err.Error(),
})
Expand All @@ -117,7 +117,7 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {

gitSSHKey, err := api.Database.GetGitSSHKey(r.Context(), workspace.OwnerID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching git SSH key.",
Detail: err.Error(),
})
Expand Down
42 changes: 9 additions & 33 deletions coderd/httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"strings"

"github.com/go-playground/validator/v10"

"github.com/coder/coder/codersdk"
)

var (
Expand Down Expand Up @@ -50,42 +52,16 @@ func init() {
}
}

// Response represents a generic HTTP response.
type Response struct {
// Message is an actionable message that depicts actions the request took.
// These messages should be fully formed sentences with proper punctuation.
// Examples:
// - "A user has been created."
// - "Failed to create a user."
Message string `json:"message"`
// Detail is a debug message that provides further insight into why the
// action failed. This information can be technical and a regular golang
// err.Error() text.
// - "database: too many open connections"
// - "stat: too many open files"
Detail string `json:"detail,omitempty"`
// Validations are form field-specific friendly error messages. They will be
// shown on a form field in the UI. These can also be used to add additional
// context if there is a set of errors in the primary 'Message'.
Validations []Error `json:"validations,omitempty"`
}

// Error represents a scoped error to a user input.
type Error struct {
Field string `json:"field" validate:"required"`
Detail string `json:"detail" validate:"required"`
}

// ResourceNotFound is intentionally vague. All 404 responses should be identical
// to prevent leaking existence of resources.
func ResourceNotFound(rw http.ResponseWriter) {
Write(rw, http.StatusNotFound, Response{
Write(rw, http.StatusNotFound, codersdk.Response{
Message: "Resource not found or you do not have access to this resource",
})
}

func Forbidden(rw http.ResponseWriter) {
Write(rw, http.StatusForbidden, Response{
Write(rw, http.StatusForbidden, codersdk.Response{
Message: "Forbidden.",
})
}
Expand Down Expand Up @@ -114,7 +90,7 @@ func Write(rw http.ResponseWriter, status int, response interface{}) {
func Read(rw http.ResponseWriter, r *http.Request, value interface{}) bool {
err := json.NewDecoder(r.Body).Decode(value)
if err != nil {
Write(rw, http.StatusBadRequest, Response{
Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Request body must be valid JSON.",
Detail: err.Error(),
})
Expand All @@ -123,21 +99,21 @@ func Read(rw http.ResponseWriter, r *http.Request, value interface{}) bool {
err = validate.Struct(value)
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
apiErrors := make([]Error, 0, len(validationErrors))
apiErrors := make([]codersdk.Error, 0, len(validationErrors))
for _, validationError := range validationErrors {
apiErrors = append(apiErrors, Error{
apiErrors = append(apiErrors, codersdk.Error{
Field: validationError.Field(),
Detail: fmt.Sprintf("Validation failed for tag %q with value: \"%v\"", validationError.Tag(), validationError.Value()),
})
}
Write(rw, http.StatusBadRequest, Response{
Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Validation failed.",
Validations: apiErrors,
})
return false
}
if err != nil {
Write(rw, http.StatusInternalServerError, Response{
Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error validating request body payload.",
Detail: err.Error(),
})
Expand Down
5 changes: 3 additions & 2 deletions coderd/httpapi/httpapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import (
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
)

func TestWrite(t *testing.T) {
t.Parallel()
t.Run("NoErrors", func(t *testing.T) {
t.Parallel()
rw := httptest.NewRecorder()
httpapi.Write(rw, http.StatusOK, httpapi.Response{
httpapi.Write(rw, http.StatusOK, codersdk.Response{
Message: "Wow.",
})
var m map[string]interface{}
Expand Down Expand Up @@ -71,7 +72,7 @@ func TestRead(t *testing.T) {

var validate toValidate
require.False(t, httpapi.Read(rw, r, &validate))
var v httpapi.Response
var v codersdk.Response
err := json.NewDecoder(rw.Body).Decode(&v)
require.NoError(t, err)
require.Len(t, v.Validations, 1)
Expand Down
14 changes: 8 additions & 6 deletions coderd/httpapi/queryparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"github.com/google/uuid"

"github.com/coder/coder/codersdk"

"golang.org/x/xerrors"
)

Expand All @@ -17,19 +19,19 @@ import (
type QueryParamParser struct {
// Errors is the set of errors to return via the API. If the length
// of this set is 0, there are no errors!.
Errors []Error
Errors []codersdk.Error
}

func NewQueryParamParser() *QueryParamParser {
return &QueryParamParser{
Errors: []Error{},
Errors: []codersdk.Error{},
}
}

func (p *QueryParamParser) Int(vals url.Values, def int, queryParam string) int {
v, err := parseQueryParam(vals, strconv.Atoi, def, queryParam)
if err != nil {
p.Errors = append(p.Errors, Error{
p.Errors = append(p.Errors, codersdk.Error{
Field: queryParam,
Detail: fmt.Sprintf("Query param %q must be a valid integer (%s)", queryParam, err.Error()),
})
Expand All @@ -47,7 +49,7 @@ func (p *QueryParamParser) UUIDorMe(vals url.Values, def uuid.UUID, me uuid.UUID
func (p *QueryParamParser) UUID(vals url.Values, def uuid.UUID, queryParam string) uuid.UUID {
v, err := parseQueryParam(vals, uuid.Parse, def, queryParam)
if err != nil {
p.Errors = append(p.Errors, Error{
p.Errors = append(p.Errors, codersdk.Error{
Field: queryParam,
Detail: fmt.Sprintf("Query param %q must be a valid uuid", queryParam),
})
Expand Down Expand Up @@ -75,7 +77,7 @@ func (p *QueryParamParser) UUIDs(vals url.Values, def []uuid.UUID, queryParam st
return ids, nil
}, def, queryParam)
if err != nil {
p.Errors = append(p.Errors, Error{
p.Errors = append(p.Errors, codersdk.Error{
Field: queryParam,
Detail: fmt.Sprintf("Query param %q has invalid uuids: %q", queryParam, err.Error()),
})
Expand Down Expand Up @@ -105,7 +107,7 @@ func (*QueryParamParser) Strings(vals url.Values, def []string, queryParam strin
func ParseCustom[T any](parser *QueryParamParser, vals url.Values, def T, queryParam string, parseFunc func(v string) (T, error)) T {
v, err := parseQueryParam(vals, parseFunc, def, queryParam)
if err != nil {
parser.Errors = append(parser.Errors, Error{
parser.Errors = append(parser.Errors, codersdk.Error{
Field: queryParam,
Detail: fmt.Sprintf("Query param %q has invalid value: %s", queryParam, err.Error()),
})
Expand Down
Loading