improve error handling

This commit is contained in:
Hayden 2022-09-24 11:14:36 -08:00
parent d7e589a10d
commit 29d7c277d3
10 changed files with 184 additions and 79 deletions

View file

@ -274,6 +274,15 @@ const docTemplate = `{
"schema": { "schema": {
"$ref": "#/definitions/types.ItemOut" "$ref": "#/definitions/types.ItemOut"
} }
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/server.ValidationError"
}
}
} }
} }
} }
@ -1025,6 +1034,17 @@ const docTemplate = `{
} }
} }
}, },
"server.ValidationError": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"types.ApiSummary": { "types.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -266,6 +266,15 @@
"schema": { "schema": {
"$ref": "#/definitions/types.ItemOut" "$ref": "#/definitions/types.ItemOut"
} }
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/server.ValidationError"
}
}
} }
} }
} }
@ -1017,6 +1026,17 @@
} }
} }
}, },
"server.ValidationError": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"types.ApiSummary": { "types.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -14,6 +14,13 @@ definitions:
items: items:
type: any type: any
type: object type: object
server.ValidationError:
properties:
field:
type: string
reason:
type: string
type: object
types.ApiSummary: types.ApiSummary:
properties: properties:
build: build:
@ -543,6 +550,12 @@ paths:
description: OK description: OK
schema: schema:
$ref: '#/definitions/types.ItemOut' $ref: '#/definitions/types.ItemOut'
"422":
description: Unprocessable Entity
schema:
items:
$ref: '#/definitions/server.ValidationError'
type: array
security: security:
- Bearer: [] - Bearer: []
summary: imports items into the database summary: imports items into the database

View file

@ -43,7 +43,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
// API Version 1 // API Version 1
v1Base := v1.BaseUrlFunc(prefix) v1Base := v1.BaseUrlFunc(prefix)
v1Ctrl := v1.NewControllerV1(a.services) v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
{ {
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{ r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{
Version: Version, Version: Version,

View file

@ -8,8 +8,15 @@ import (
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/homebox/backend/pkgs/server"
) )
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
return func(ctrl *V1Controller) {
ctrl.maxUploadSize = maxUploadSize
}
}
type V1Controller struct { type V1Controller struct {
svc *services.AllServices svc *services.AllServices
maxUploadSize int64
} }
func BaseUrlFunc(prefix string) func(s string) string { func BaseUrlFunc(prefix string) func(s string) string {
@ -21,7 +28,7 @@ func BaseUrlFunc(prefix string) func(s string) string {
return prefixFunc return prefixFunc
} }
func NewControllerV1(svc *services.AllServices) *V1Controller { func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) *V1Controller {
ctrl := &V1Controller{ ctrl := &V1Controller{
svc: svc, svc: svc,
} }

View file

@ -2,10 +2,8 @@ package v1
import ( import (
"encoding/csv" "encoding/csv"
"errors"
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/services" "github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types" "github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/homebox/backend/pkgs/server"
@ -191,65 +189,3 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
server.Respond(w, http.StatusNoContent, nil) server.Respond(w, http.StatusNoContent, nil)
} }
} }
// HandleItemsImport godocs
// @Summary imports items into the database
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} types.ItemOut
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Max upload size of 10 MB - TODO: Set via config
err := r.ParseMultipartForm(10 << 20)
if err != nil {
log.Err(err).Msg("failed to parse multipart form")
server.RespondServerError(w)
return
}
file, _, err := r.FormFile("file")
if err != nil {
log.Err(err).Msg("failed to get file from form")
server.RespondServerError(w)
return
}
attachmentName := r.FormValue("name")
if attachmentName == "" {
log.Err(err).Msg("failed to get name from form")
server.RespondError(w, http.StatusBadRequest, errors.New("name is required"))
}
attachmentType := r.FormValue("type")
if attachmentType == "" {
attachmentName = "attachment"
}
uid, _, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.AttachmentAdd(
ctx,
uid,
attachmentName,
attachment.Type(attachmentType),
file,
)
if err != nil {
log.Err(err).Msg("failed to add attachment")
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusCreated, item)
}
}

View file

@ -1,18 +1,97 @@
package v1 package v1
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath" "path/filepath"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/services" "github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types" "github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// HandleItemsImport godocs
// @Summary imports items into the database
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} types.ItemOut
// @Failure 422 {object} []server.ValidationError
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil {
log.Err(err).Msg("failed to parse multipart form")
server.RespondError(w, http.StatusBadRequest, errors.New("failed to parse multipart form"))
return
}
errs := make(server.ValidationErrors, 0)
file, _, err := r.FormFile("file")
if err != nil {
switch {
case errors.Is(err, http.ErrMissingFile):
log.Debug().Msg("file for attachment is missing")
errs = errs.Append("file", "file is required")
default:
log.Err(err).Msg("failed to get file from form")
server.RespondServerError(w)
return
}
}
attachmentName := r.FormValue("name")
if attachmentName == "" {
log.Debug().Msg("failed to get name from form")
errs = errs.Append("name", "name is required")
}
if errs.HasErrors() {
server.Respond(w, http.StatusUnprocessableEntity, errs)
return
}
attachmentType := r.FormValue("type")
if attachmentType == "" {
attachmentType = attachment.TypeAttachment.String()
}
id, _, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.AttachmentAdd(
ctx,
id,
attachmentName,
attachment.Type(attachmentType),
file,
)
if err != nil {
log.Err(err).Msg("failed to add attachment")
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusCreated, item)
}
}
// HandleItemAttachmentGet godocs // HandleItemAttachmentGet godocs
// @Summary retrieves an attachment for an item // @Summary retrieves an attachment for an item
// @Tags Items // @Tags Items

View file

@ -58,7 +58,7 @@ func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.U
} }
if _, err := os.Stat(attachment.Edges.Document.Path); os.IsNotExist(err) { if _, err := os.Stat(attachment.Edges.Document.Path); os.IsNotExist(err) {
svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId) _ = svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
return "", ErrNotFound return "", ErrNotFound
} }

View file

@ -4,22 +4,47 @@ import (
"net/http" "net/http"
) )
type ValidationError struct {
Field string `json:"field"`
Reason string `json:"reason"`
}
type ValidationErrors []ValidationError
func (ve *ValidationErrors) HasErrors() bool {
if (ve == nil) || (len(*ve) == 0) {
return false
}
for _, err := range *ve {
if err.Field != "" {
return true
}
}
return false
}
func (ve ValidationErrors) Append(field, reasons string) ValidationErrors {
return append(ve, ValidationError{
Field: field,
Reason: reasons,
})
}
// ErrorBuilder is a helper type to build a response that contains an array of errors. // ErrorBuilder is a helper type to build a response that contains an array of errors.
// Typical use cases are for returning an array of validation errors back to the user. // Typical use cases are for returning an array of validation errors back to the user.
// //
// Example: // Example:
// //
// // {
// { // "errors": [
// "errors": [ // "invalid id",
// "invalid id", // "invalid name",
// "invalid name", // "invalid description"
// "invalid description" // ],
// ], // "message": "Unprocessable Entity",
// "message": "Unprocessable Entity", // "status": 422
// "status": 422 // }
// }
//
type ErrorBuilder struct { type ErrorBuilder struct {
errs []string errs []string
} }

View file

@ -21,6 +21,11 @@ export interface ServerResults {
items: any; items: any;
} }
export interface ServerValidationError {
field: string;
reason: string;
}
export interface ApiSummary { export interface ApiSummary {
build: Build; build: Build;
health: boolean; health: boolean;