diff --git a/HISTORY.rst b/HISTORY.rst index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_SElTVE9SWS5yc3Q=..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_SElTVE9SWS5yc3Q= 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -47,6 +47,8 @@ - preview: fix metadata not being passed to rendering engine +- render: return detailed errors + 0.8.0 (2024-12-09) ================== diff --git a/lib/render.go b/lib/render.go index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_bGliL3JlbmRlci5nbw==..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_bGliL3JlbmRlci5nbw== 100644 --- a/lib/render.go +++ b/lib/render.go @@ -197,8 +197,10 @@ for _, record := range data { var doc models.Document + result = append(result, &doc) + w := rendering.NewDocumentWriter(&doc) if err := pipeline.Render(ctx, w, record); err != nil { pipelineTimer.ObserveDurationWithLabelValues( templateLanguage, fromType, toType, "failure") @@ -200,8 +202,12 @@ w := rendering.NewDocumentWriter(&doc) if err := pipeline.Render(ctx, w, record); err != nil { pipelineTimer.ObserveDurationWithLabelValues( templateLanguage, fromType, toType, "failure") + if errors.Is(err, ErrInvalidInput) { + return nil, models.NewRenderFailedError(err, result) + } + return nil, err } @@ -212,8 +218,6 @@ }) } } - - result = append(result, &doc) } } @@ -228,8 +232,10 @@ if len(pipeline) != 0 { var output models.Document + result = append(result, &output) + reader := rendering.NewDocumentReader(document) writer := rendering.NewDocumentWriter(&output) userContext := SetUsername(ctx, username) if err := pipeline.Render(userContext, writer, reader); err != nil { @@ -231,8 +237,12 @@ reader := rendering.NewDocumentReader(document) writer := rendering.NewDocumentWriter(&output) userContext := SetUsername(ctx, username) if err := pipeline.Render(userContext, writer, reader); err != nil { + if errors.Is(err, ErrInvalidInput) { + return nil, models.NewRenderFailedError(err, result) + } + return nil, err } @@ -243,8 +253,6 @@ }) } } - - result = append(result, &output) } else { for _, cb := range r.onDocRender { if err := cb(ctx, &db, document); err != nil { diff --git a/models/render_failed_doc_details.go b/models/render_failed_doc_details.go new file mode 100644 index 0000000000000000000000000000000000000000..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_bW9kZWxzL3JlbmRlcl9mYWlsZWRfZG9jX2RldGFpbHMuZ28= --- /dev/null +++ b/models/render_failed_doc_details.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// RenderFailedDocDetails render failed doc details +// +// swagger:model render-failed-doc-details +type RenderFailedDocDetails struct { + + // metadata + Metadata Metadata `json:"metadata,omitempty"` + + // A list of errors that occured during rendering + RenderErrors []RenderError `json:"render-errors"` + + // The document mimetype + Type string `json:"type,omitempty"` +} + +// Validate validates this render failed doc details +func (m *RenderFailedDocDetails) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMetadata(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRenderErrors(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RenderFailedDocDetails) validateMetadata(formats strfmt.Registry) error { + + if swag.IsZero(m.Metadata) { // not required + return nil + } + + if err := m.Metadata.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("metadata") + } + return err + } + + return nil +} + +func (m *RenderFailedDocDetails) validateRenderErrors(formats strfmt.Registry) error { + + if swag.IsZero(m.RenderErrors) { // not required + return nil + } + + for i := 0; i < len(m.RenderErrors); i++ { + + if err := m.RenderErrors[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("render-errors" + "." + strconv.Itoa(i)) + } + return err + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *RenderFailedDocDetails) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *RenderFailedDocDetails) UnmarshalBinary(b []byte) error { + var res RenderFailedDocDetails + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/render_failed_error.go b/models/render_failed_error.go new file mode 100644 index 0000000000000000000000000000000000000000..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_bW9kZWxzL3JlbmRlcl9mYWlsZWRfZXJyb3IuZ28= --- /dev/null +++ b/models/render_failed_error.go @@ -0,0 +1,83 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// RenderFailedError Rendering failure +// +// swagger:model render-failed-error +type RenderFailedError struct { + + // Detailed errors reported by the rendering engines for each document + Details []*RenderFailedDocDetails `json:"details"` + + // The main reason why the rendering failed + Message string `json:"message,omitempty"` +} + +// Validate validates this render failed error +func (m *RenderFailedError) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateDetails(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RenderFailedError) validateDetails(formats strfmt.Registry) error { + + if swag.IsZero(m.Details) { // not required + return nil + } + + for i := 0; i < len(m.Details); i++ { + if swag.IsZero(m.Details[i]) { // not required + continue + } + + if m.Details[i] != nil { + if err := m.Details[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("details" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *RenderFailedError) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *RenderFailedError) UnmarshalBinary(b []byte) error { + var res RenderFailedError + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/render_failed_error_extra.go b/models/render_failed_error_extra.go new file mode 100644 index 0000000000000000000000000000000000000000..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_bW9kZWxzL3JlbmRlcl9mYWlsZWRfZXJyb3JfZXh0cmEuZ28= --- /dev/null +++ b/models/render_failed_error_extra.go @@ -0,0 +1,20 @@ +package models + +func NewRenderFailedError(err error, docs []*Document) *RenderFailedError { + r := RenderFailedError{ + Message: err.Error(), + Details: make([]*RenderFailedDocDetails, len(docs)), + } + + for i, doc := range docs { + r.Details[i].Type = doc.Type + r.Details[i].Metadata = doc.Metadata + r.Details[i].RenderErrors = doc.RenderErrors + } + + return &r +} + +func (err *RenderFailedError) Error() string { + return err.Message +} diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_cmVzdGFwaS9lbWJlZGRlZF9zcGVjLmdv..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_cmVzdGFwaS9lbWJlZGRlZF9zcGVjLmdv 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -250,6 +250,12 @@ } } }, + "400": { + "description": "A blocking rendering error occured", + "schema": { + "$ref": "#/definitions/render-failed-error" + } + }, "401": { "$ref": "#/responses/unauthorized" }, @@ -1145,6 +1151,43 @@ }, "x-isnullable": false }, + "render-failed-doc-details": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "render-errors": { + "description": "A list of errors that occured during rendering", + "type": "array", + "items": { + "$ref": "#/definitions/render-error" + } + }, + "type": { + "description": "The document mimetype", + "type": "string", + "example": "text/html" + } + } + }, + "render-failed-error": { + "description": "Rendering failure", + "type": "object", + "properties": { + "details": { + "description": "Detailed errors reported by the rendering engines for each document", + "type": "array", + "items": { + "$ref": "#/definitions/render-failed-doc-details" + } + }, + "message": { + "description": "The main reason why the rendering failed", + "type": "string" + } + } + }, "render-request": { "type": "object", "properties": { @@ -1640,6 +1683,12 @@ } } }, + "400": { + "description": "A blocking rendering error occured", + "schema": { + "$ref": "#/definitions/render-failed-error" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -2664,6 +2713,43 @@ }, "x-isnullable": false }, + "render-failed-doc-details": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "render-errors": { + "description": "A list of errors that occured during rendering", + "type": "array", + "items": { + "$ref": "#/definitions/render-error" + } + }, + "type": { + "description": "The document mimetype", + "type": "string", + "example": "text/html" + } + } + }, + "render-failed-error": { + "description": "Rendering failure", + "type": "object", + "properties": { + "details": { + "description": "Detailed errors reported by the rendering engines for each document", + "type": "array", + "items": { + "$ref": "#/definitions/render-failed-doc-details" + } + }, + "message": { + "description": "The main reason why the rendering failed", + "type": "string" + } + } + }, "render-request": { "type": "object", "properties": { diff --git a/restapi/handlers/render.go b/restapi/handlers/render.go index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_cmVzdGFwaS9oYW5kbGVycy9yZW5kZXIuZ28=..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_cmVzdGFwaS9oYW5kbGVycy9yZW5kZXIuZ28= 100644 --- a/restapi/handlers/render.go +++ b/restapi/handlers/render.go @@ -40,5 +40,7 @@ ctx, principal.Subject, template, data, document, metadata, toType, principal.EnabledRequestLogging, ) - if errors.Is(err, redner.ErrInvalidInput) { + + switch { + case errors.Is(err, redner.ErrInvalidInput): return nil, utils.HTTPBadRequest(err) @@ -44,5 +46,5 @@ return nil, utils.HTTPBadRequest(err) - } else if err != nil { + case err != nil: return nil, err } @@ -76,6 +78,11 @@ return r } + var renderFailedError *models.RenderFailedError + if errors.As(err, &renderFailedError) { + return op.NewRenderBadRequest().WithPayload(renderFailedError) + } + if params.HTTPRequest.Context().Err() != nil { return op.NewRenderDefault(499) } diff --git a/restapi/operations/rendering/render_responses.go b/restapi/operations/rendering/render_responses.go index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_cmVzdGFwaS9vcGVyYXRpb25zL3JlbmRlcmluZy9yZW5kZXJfcmVzcG9uc2VzLmdv..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_cmVzdGFwaS9vcGVyYXRpb25zL3JlbmRlcmluZy9yZW5kZXJfcmVzcG9uc2VzLmdv 100644 --- a/restapi/operations/rendering/render_responses.go +++ b/restapi/operations/rendering/render_responses.go @@ -60,6 +60,50 @@ } } +// RenderBadRequestCode is the HTTP code returned for type RenderBadRequest +const RenderBadRequestCode int = 400 + +/*RenderBadRequest A blocking rendering error occured + +swagger:response renderBadRequest +*/ +type RenderBadRequest struct { + + /* + In: Body + */ + Payload *models.RenderFailedError `json:"body,omitempty"` +} + +// NewRenderBadRequest creates RenderBadRequest with default headers values +func NewRenderBadRequest() *RenderBadRequest { + + return &RenderBadRequest{} +} + +// WithPayload adds the payload to the render bad request response +func (o *RenderBadRequest) WithPayload(payload *models.RenderFailedError) *RenderBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the render bad request response +func (o *RenderBadRequest) SetPayload(payload *models.RenderFailedError) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RenderBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + // RenderUnauthorizedCode is the HTTP code returned for type RenderUnauthorized const RenderUnauthorizedCode int = 401 diff --git a/swagger.yaml b/swagger.yaml index 7bca2c685c50b7e1cec5c9345a2e2546dee287d8_c3dhZ2dlci55YW1s..2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8_c3dhZ2dlci55YW1s 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -632,6 +632,10 @@ $ref: "#/definitions/document" 401: $ref: '#/responses/unauthorized' + 400: + description: A blocking rendering error occured + schema: + $ref: '#/definitions/render-failed-error' default: $ref: '#/responses/default' @@ -718,6 +722,34 @@ message: type: string + render-failed-doc-details: + type: object + properties: + type: + type: string + description: The document mimetype + example: text/html + metadata: + $ref: "#/definitions/metadata" + render-errors: + type: array + description: A list of errors that occured during rendering + items: + $ref: "#/definitions/render-error" + + render-failed-error: + type: object + description: Rendering failure + properties: + message: + type: string + description: The main reason why the rendering failed + details: + type: array + description: Detailed errors reported by the rendering engines for each document + items: + $ref: "#/definitions/render-failed-doc-details" + credentials: type: object description: Credentials for authentication