Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
Merge pull request #42 from linksmart/problem-details
Browse files Browse the repository at this point in the history
Use Problem Details for error responses
  • Loading branch information
farshidtz authored Mar 19, 2021
2 parents 643f029 + 6605c48 commit 771d329
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 64 deletions.
63 changes: 56 additions & 7 deletions apidoc/openapi-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ paths:
schema:
type: string
'400':
$ref: '#/components/responses/RespBadRequest'
$ref: '#/components/responses/RespValidationBadRequest'
'401':
$ref: '#/components/responses/RespUnauthorized'
'403':
Expand Down Expand Up @@ -380,37 +380,51 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
RespValidationBadRequest:
description: Bad Request (e.g. validation error)
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ProblemDetails'
- $ref: '#/components/schemas/ValidationError'
ValidationErrorResponse:
description: Invalid Thing Description
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
RespUnauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
RespForbidden:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
RespNotfound:
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
RespConflict:
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
RespInternalServerError:
description: Internal Server Error
content:
application/ld+json:
schema:
$ref: '#/components/schemas/ErrorResponse'
$ref: '#/components/schemas/ProblemDetails'
schemas:
ErrorResponse:
type: object
Expand All @@ -419,6 +433,41 @@ components:
type: integer
message:
type: string
ProblemDetails:
description: RFC7807 Problem Details (https://tools.ietf.org/html/rfc7807)
properties:
# type:
# type: string
# description: A URI reference that identifies the problem type.
status:
type: integer
format: int32
description: The HTTP status code.
title:
type: string
description: A short, human-readable summary of the problem type.
detail:
type: string
description: A human-readable explanation specific to this occurrence of the problem
instance:
type: string
description: A URI reference that identifies the specific occurrence of the problem.\
ValidationError:
description: Thing Description validation error
allOf:
- $ref: '#/components/schemas/ProblemDetails'
- type: object
properties:
validationErrors:
type: array
items:
type: object
properties:
field:
type: string
description:
type: string

ThingDescription:
description: WoT Thing Description
type: object
Expand Down
14 changes: 11 additions & 3 deletions catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@ const (
_ttl = "ttl"
)

func validateThingDescription(td map[string]interface{}) error {
func validateThingDescription(td map[string]interface{}) ([]wot.ValidationError, error) {
issues, err := wot.ValidateMap(&td)
if err != nil {
return nil, fmt.Errorf("error validating with JSON schema: %s", err)
}

if td[_ttl] != nil {
_, ok := td[_ttl].(float64)
if !ok {
return fmt.Errorf("ttl is %T instead of float64", td[_ttl])
issues = append(issues, wot.ValidationError{
Field: _ttl,
Descr: fmt.Sprintf("Invalid type. Expected float64, given: %T", td[_ttl]),
})
}
}

return wot.ValidateMap(&td)
return issues, nil
}

// Controller interface
Expand Down
19 changes: 15 additions & 4 deletions catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (

// here are only the tests related to non-standard TD vocabulary
func TestValidateThingDescription(t *testing.T) {
err := loadSchema()
if err != nil {
t.Fatalf("error loading WoT Thing Description schema: %s", err)
}

t.Run("non-float TTL", func(t *testing.T) {
var td = map[string]any{
"@context": "https://www.w3.org/2019/wot/td/v1",
Expand All @@ -20,14 +25,20 @@ func TestValidateThingDescription(t *testing.T) {
},
"ttl": 1,
}
err := validateThingDescription(td)
if err == nil {
results, err := validateThingDescription(td)
if err != nil {
t.Fatalf("internal validation error: %s", err)
}
if len(results) == 0 {
t.Fatalf("Didn't return error on integer TTL.")
}

td["ttl"] = "1"
err = validateThingDescription(td)
if err == nil {
results, err = validateThingDescription(td)
if err != nil {
t.Fatalf("internal validation error: %s", err)
}
if len(results) == 0 {
t.Fatalf("Didn't return error on string TTL.")
}
})
Expand Down
27 changes: 20 additions & 7 deletions catalog/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ func (c *Controller) add(td ThingDescription) (string, error) {
id = c.newURN()
td[_id] = id
}
if err := validateThingDescription(td); err != nil {
return "", &BadRequestError{err.Error()}

results, err := validateThingDescription(td)
if err != nil {
return "", err
}
if len(results) != 0 {
return "", &ValidationError{results}
}

td[_created] = time.Now().UTC()
td[_modified] = td[_created]

err := c.storage.add(id, td)
err = c.storage.add(id, td)
if err != nil {
return "", err
}
Expand All @@ -66,8 +71,12 @@ func (c *Controller) update(id string, td ThingDescription) error {
return err
}

if err := validateThingDescription(td); err != nil {
return &BadRequestError{err.Error()}
results, err := validateThingDescription(td)
if err != nil {
return err
}
if len(results) != 0 {
return &ValidationError{results}
}

td[_created] = oldTD[_created]
Expand Down Expand Up @@ -111,8 +120,12 @@ func (c *Controller) patch(id string, td ThingDescription) error {
return err
}

if err := validateThingDescription(td); err != nil {
return &BadRequestError{err.Error()}
results, err := validateThingDescription(td)
if err != nil {
return err
}
if len(results) != 0 {
return &ValidationError{results}
}

td[_modified] = time.Now().UTC()
Expand Down
55 changes: 38 additions & 17 deletions catalog/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ package catalog

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"

"github.com/linksmart/thing-directory/wot"
uuid "github.com/satori/go.uuid"
)

// Not Found
Expand All @@ -24,30 +27,48 @@ type BadRequestError struct{ s string }

func (e *BadRequestError) Error() string { return e.s }

// Error describes an API error (serializable in JSON)
type Error struct {
// Code is the (http) code of the error
Code int `json:"code"`
// Message is the (human-readable) error message
Message string `json:"message"`
// Validation error (HTTP Bad Request)
type ValidationError struct {
validationErrors []wot.ValidationError
}

func (e *ValidationError) Error() string { return "validation errors" }

// ErrorResponse writes error to HTTP ResponseWriter
func ErrorResponse(w http.ResponseWriter, code int, msg ...interface{}) {
ProblemDetailsResponse(w, wot.ProblemDetails{
Status: code,
Detail: fmt.Sprint(msg...),
})
}

func ValidationErrorResponse(w http.ResponseWriter, validationIssues []wot.ValidationError) {
ProblemDetailsResponse(w, wot.ProblemDetails{
Status: http.StatusBadRequest,
Detail: "The input did not pass the JSON Schema validation",
ValidationErrors: validationIssues,
})
}

// ErrorResponse writes error to HTTP ResponseWriter
func ErrorResponse(w http.ResponseWriter, code int, msgs ...string) {
msg := strings.Join(msgs, " ")
e := &Error{
code,
msg,
func ProblemDetailsResponse(w http.ResponseWriter, pd wot.ProblemDetails) {
if pd.Title == "" {
pd.Title = http.StatusText(pd.Status)
if pd.Title == "" {
panic(fmt.Sprint("Invalid HTTP status code: ", pd.Status))
}
}
if code >= 500 {
log.Println("ERROR:", msg)
pd.Instance = "/errors/" + uuid.NewV4().String()
log.Println("Problem Details instance:", pd.Instance)
if pd.Status >= 500 {
log.Println("ERROR:", pd.Detail)
}
b, err := json.Marshal(e)
b, err := json.Marshal(pd)
if err != nil {
log.Printf("ERROR serializing error object: %s", err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(pd.Status)
_, err = w.Write(b)
if err != nil {
log.Printf("ERROR writing HTTP response: %s", err)
Expand Down
26 changes: 20 additions & 6 deletions catalog/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

"github.com/gorilla/mux"
"github.com/linksmart/service-catalog/v3/utils"
"github.com/linksmart/thing-directory/wot"
)

const (
Expand Down Expand Up @@ -88,6 +87,9 @@ func (a *HTTPAPI) Post(w http.ResponseWriter, req *http.Request) {
case *BadRequestError:
ErrorResponse(w, http.StatusBadRequest, "Invalid registration:", err.Error())
return
case *ValidationError:
ValidationErrorResponse(w, err.(*ValidationError).validationErrors)
return
default:
ErrorResponse(w, http.StatusInternalServerError, "Error creating the registration:", err.Error())
return
Expand Down Expand Up @@ -139,6 +141,9 @@ func (a *HTTPAPI) Put(w http.ResponseWriter, req *http.Request) {
case *BadRequestError:
ErrorResponse(w, http.StatusBadRequest, "Invalid registration:", err.Error())
return
case *ValidationError:
ValidationErrorResponse(w, err.(*ValidationError).validationErrors)
return
default:
ErrorResponse(w, http.StatusInternalServerError, "Error creating the registration:", err.Error())
return
Expand All @@ -151,6 +156,9 @@ func (a *HTTPAPI) Put(w http.ResponseWriter, req *http.Request) {
case *BadRequestError:
ErrorResponse(w, http.StatusBadRequest, "Invalid registration:", err.Error())
return
case *ValidationError:
ValidationErrorResponse(w, err.(*ValidationError).validationErrors)
return
default:
ErrorResponse(w, http.StatusInternalServerError, "Error updating the registration:", err.Error())
return
Expand Down Expand Up @@ -193,6 +201,9 @@ func (a *HTTPAPI) Patch(w http.ResponseWriter, req *http.Request) {
case *BadRequestError:
ErrorResponse(w, http.StatusBadRequest, "Invalid registration:", err.Error())
return
case *ValidationError:
ValidationErrorResponse(w, err.(*ValidationError).validationErrors)
return
default:
ErrorResponse(w, http.StatusInternalServerError, "Error updating the registration:", err.Error())
return
Expand Down Expand Up @@ -429,11 +440,14 @@ func (a *HTTPAPI) GetValidation(w http.ResponseWriter, req *http.Request) {
}

var response ValidationResult
if err := validateThingDescription(td); err != nil {
if verr, ok := err.(*wot.ValidationError); ok {
response.Errors = verr.Errors
} else {
response.Errors = []string{err.Error()}
results, err := validateThingDescription(td)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
if len(results) != 0 {
for _, result := range results {
response.Errors = append(response.Errors, fmt.Sprintf("%s: %s", result.Field, result.Descr))
}
} else {
response.Valid = true
Expand Down
Loading

0 comments on commit 771d329

Please sign in to comment.