Messaging System for Transferring Error, Warning, and Information Messages from OData V4 Services to HTTP Client Applications

coauthor: Ralf Handl

SAP Fiori Elements Object Page Floorplan Draft Scenario Displaying an Error Message

SAP Fiori Elements Object Page Floorplan Draft Scenario Displaying an Error Message

Google Chrome DevTools Displaying an OData Error Message

OData Error Message

Introduction

When building applications with the SAP Cloud Application Programming Model (CAP), generic service providers significantly shorten the service development time by providing many out-of-the-box solutions to recurring tasks allowing you to focus on the specific business logic of your application, thus reducing the overall implementation effort.

Examples of out-of-the-box solutions to recurring tasks — but not limited to — are the following:

  • Serving of create, read, update, and delete (CRUD) requests of exposed entities
  • Generic handlers implementations, including standard input validation

These capabilities allow you to develop fully-fledged running services quickly by composing Core Data Services and running simple command-line interface (CLI) commands.

However, things don’t always go straightforward when processing an OData request. HTTP-based applications, such as those using SAP Fiori Elements, send OData requests to backend systems. Assuming that a request payload is invalid or partially invalid, for example, a user might have entered a value in a field that violates a database constraint, but in another field, the user might have entered a valid value. Thus, the valid input can be stored successfully in the backend system, but the invalid input cannot be stored, and it is essential that the user is informed about this error.

This blog post will give you a brief overview of the messaging system for transferring error, warning, and information messages from OData V4 services to HTTP client applications such as those using SAP Fiori Elements.

Possible Outcomes when Processing an OData Request

In the context of HTTP clients and HTTP servers based on CAP — or similar server-side technologies — things don’t always go straightforward when processing an OData request. There are three possible outcomes:

  1. The request is processed successfully. Period.
  2. The server couldn’t process the request at all, and we want to inform the user what went wrong and why
  3. The request is processed successfully, but there were some side effects or things that went not relatively straightforward and are so essential that we have to inform the user via SAP Fiori Messaging.

OData defines how to handle the first and second scenarios, but the standard does not mandate how to handle the third scenario. Now focusing on the third scenario, for example, if in an SAP Fiori Elements Object Page Floorplan draft scenario, a user doesn’t fill all mandatory text input fields and tries to activate/save the draft to store the active version of a business entity in the back-end system. In that scenario, errors will occur that prevent the entity from being stored, and the user must be able to see possible error messages on the screen.

Error Response Body in OData

Block Diagram of Messaging System for Transferring Error Messages from an OData V4 Service to an HTTP Client Application

Block Diagram of Messaging System for Transferring an Error Messages from an OData V4 Services to an SAP Fiori Elements Based Application

The HTTP response body’s content adheres to the standard OData specification for an error response body in addition to an SAP-specific format. So errors, warnings, and info messages are transferred to HTTP clients in a format that reflects the most common requirements for messages targeted at users.

The OData HTTP error response body is a JSON object with a single name/value pair named error that contains the following name/value pairs:

    • A machine-readable error code — a language-independent string
    • A human-readable, language-dependent message representation of the error summarizing the problem
    • An optional target — a relative resource path to correlate the error message
    • An array of details, each with a code, message, and target
    • An optional innererror structured instance with service-defined content

Additionally, for extensibility reasons,

implementations can add custom annotations of the form @namespace.termname or property@namespace.termname to any JSON object, where property MAY or MAY NOT match the name of a name/value pair within the JSON object.

In CAP, the error response body is extended using the @Common.numericSeverity instance annotation to add a severity to the message.

Values for the @Common.numericSeverity instance annotation

Severity Numeric severity Description
Success 1 Success — no action required
Info 2 Information — no action required
Warning 3 Warning — action may be required
Error 4 Error — action is required

Common Format for Error Target

The target of an error/warning response is always a relative resource path segment that is appended to the path part of the request URL (for GET, PATCH, PUT, and DELETE requests) or the Location response header (for POST requests that create a new entity), resulting in an OData request URL that is used to retrieve the target of the error message.

For GET, PATCH, PUT, and DELETE requests to a single entity or complex-type instance, the target is:

  • Empty — if the error is related to the addressed resource as a whole —, for example, a Sales Order, or
  • A property path relative to the addressed resource, that is to say, if a forward slash followed by the value of the target is appended to the path part of the request URL, the result is an OData request URL identifying the target of the error message
  • For POST requests that create a new entity, the target is relative to the Location response header identifying the newly created resource. Otherwise, it follows the rules for GET, PATCH, PUT, and DELETE requests to a single entity.
    Note: this includes the creation of dependent entities via a multi-valued navigation property, for example, POST Orders(42)/Items. The target is still relative to the newly created entity, in this case, an order item. Messages targeting the containing entity, in this case, order 42, can only be returned if there’s a to-1 navigation property back from the item to the order.
  • For all request types, the target may start with a forward slash. In this case, the target is interpreted as an OData path relative to the service root URL.

Examples

Given the following CAP based model snippet:


entity Headers { key ID : UUID; text : String; items : Composition of many Items on items.header = $self;
} entity Items { key ID : UUID; header : Association to one Headers @assert.target; text : String @mandatory;
}

Patching a mandatory field with null — assume that the item with the ID 7be6d296-9e7a-3505-b72e-4c7b98783578 exists in the database

HTTP Request


PATCH http://localhost:4004/service-name/Items(ID=7be6d296-9e7a-3505-b72e-4c7b98783578) HTTP/1.1
Accept: application/json;odata.metadata=minimal
Prefer: return=minimal
Content-Type: application/json;charset=UTF-8 { "text": null
}

HTTP Response


HTTP/1.1 400 Bad Request
OData-Version: 4.0
content-type: application/json;odata.metadata=minimal
Connection: close
Content-Length: 98 { "error": { "code": "400", "message": "Value is required", "target": "text", "@Common.numericSeverity": 4 }
}

Stdout (log message)


[cds] - PATCH /service-name/Items(ID=7be6d296-9e7a-3505-b72e-4c7b98783578) [cds] - Error: Value is required { code: 'ASSERT_NOT_NULL', target: 'text', args: [ 'text' ], entity: 'serviceName.Items', element: 'text', type: 'cds.String', value: null, numericSeverity: 4, id: '1090822', level: 'ERROR', timestamp: 1653756442547
}

Creating an item that references a non-existing header — @assert.target Constraint

HTTP Request — assume that a header with the ID "796e274a-c3de-4584-9de2-3ffd7d42d646" doesn’t exist in the database


POST http://localhost:4004/service-name/Items HTTP/1.1
Accept: application/json;odata.metadata=minimal
Prefer: return=minimal
Content-Type: application/json;charset=UTF-8 { "ID": "86b07ae1-2c9b-4a29-953c-b257f5a737f4", "text": "lorem cillum", "header_ID": "796e274a-c3de-4584-9de2-3ffd7d42d646"
}

HTTP Response


HTTP/1.1 400 Bad Request
OData-Version: 4.0
content-type: application/json;odata.metadata=minimal
Connection: close
Content-Length: 105 { "error": { "code": "400", "message": "Value doesn't exist", "target": "header_ID", "@Common.numericSeverity": 4 }
}

Stdout (log message)


[cds] - POST /service-name/Items [cds] - Error: Value doesn't exist { code: 'ASSERT_TARGET', target: 'header_ID', args: [ 'header_ID' ], entity: 'serviceName.Items', element: 'header_ID', type: 'cds.UUID', value: '796e274a-c3de-4584-9de2-3ffd7d42d646', numericSeverity: 4, id: '1090822', level: 'ERROR', timestamp: 1653756615316
}

Notice that in this case, the header managed to-one association is annotated with the @assert.target annotation to check whether the target entity referenced by the association (the reference’s target) exists. As the foreign key header_ID input does not have a corresponding primary key in the associated/referenced target entity/table, the OData service respond with an HTTP error message.

Multiple Errors

HTTP Request


POST http://localhost:4004/service-name/Items HTTP/1.1
Accept: application/json;odata.metadata=minimal
Prefer: return=minimal
Content-Type: application/json;charset=UTF-8 { "header_ID": "796e274a-c3de-4584-9de2-3ffd7d42d646"
}

HTTP Response


HTTP/1.1 400 Bad Request
OData-Version: 4.0
content-type: application/json;odata.metadata=minimal
Connection: close
Content-Length: 304 { "error": { "code": "400", "message": "Multiple errors occurred. Please see the details for more information.", "details": [ { "code": "400", "message": "Value is required", "target": "text", "@Common.numericSeverity": 4 }, { "code": "400", "message": "Value doesn't exist", "target": "header_ID", "@Common.numericSeverity": 4 } ] }
}

Stdout (log message)


[cds] - POST /service-name/Items [cds] - Error: Multiple errors occurred. Please see the details for more information. { details: [ { code: 'ASSERT_NOT_NULL', message: 'Value is required', target: 'text', args: [ 'text' ], entity: 'serviceName.Items', element: 'text', type: 'cds.String', value: undefined, numericSeverity: 4 }, { code: 'ASSERT_TARGET', message: "Value doesn't exist", target: 'header_ID', args: [ 'header_ID' ], entity: 'serviceName.Items', element: 'header_ID', type: 'cds.UUID', value: '796e274a-c3de-4584-9de2-3ffd7d42d646', numericSeverity: 4 } ], id: '1090822', level: 'ERROR', timestamp: 1653756684026
}

Deep Update

HTTP Request

POST http://localhost:4004/service-name/Headers HTTP/1.1
Accept: application/json;odata.metadata=minimal
Prefer: return=minimal
Content-Type: application/json;charset=UTF-8 { "ID": "9910905a-b331-419b-a202-7c73588a6637", "text": "cupidatat anim"
} PATCH http://localhost:4004/service-name/Headers(ID=9910905a-b331-419b-a202-7c73588a6637) HTTP/1.1
Accept: application/json;odata.metadata=minimal
Prefer: return=minimal
Content-Type: application/json;charset=UTF-8 { "text": "aliqua sint", "items": [{ "ID": "f509356d-2e1a-4501-a9fe-5435a46b4531", "header_ID": "9910905a-b331-419b-a202-7c73588a6637", "text": null }]
}

HTTP Response

HTTP/1.1 400 Bad Request
OData-Version: 4.0
content-type: application/json;odata.metadata=minimal
Connection: close
Content-Length: 145 { "error": { "code": "400", "message": "Value is required", "target": "items(ID=f509356d-2e1a-4501-a9fe-5435a46b4531)/text", "@Common.numericSeverity": 4 }
}

Stdout (log message)

[cds] - POST /service-name/Headers [cds] - PATCH /service-name/Headers(ID=9910905a-b331-419b-a202-7c73588a6637) [cds] - Error: Value is required { code: 'ASSERT_NOT_NULL', target: 'items(ID=f509356d-2e1a-4501-a9fe-5435a46b4531)/text', args: [ 'text' ], entity: 'serviceName.Items', element: 'text', type: 'cds.String', value: null, numericSeverity: 4, id: '1090822', level: 'ERROR', timestamp: 1653757487211
}