Technical Articles
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
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:
- The request is processed successfully. Period.
- The server couldn’t process the request at all, and we want to inform the user what went wrong and why
- 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 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 acode
,message
, andtarget
- An optional
innererror
structured instance with service-defined content
- A machine-readable error
Additionally, for extensibility reasons,
implementations can add custom annotations of the form
@namespace.termname
orproperty@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 theLocation
response header identifying the newly created resource. Otherwise, it follows the rules forGET
,PATCH
,PUT
, andDELETE
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
}
Thanks Arley for such nice explanation
Kindly let me know if there any way to frame custom error messages in the handler ( before, on and after events)
In case of multiple errors , how to pass error messages in req.Error function
Thanks once again
You can find detailed information about handling errors in CAP in the official documentation at:
https://cap.cloud.sap/docs/node.js/events#req-error
Error messages are collected within the `req.errors` property, an array-like data structure.
Pass a custom error message to `req.error()`:
Thanks Arley for summarising the targets in different scenario.
We have oData V4 in our application. We currently have a scenario where we try to create multiple entities using one batch call (one batch call contains multiple post requests). Now the success response for those may contain information message for all or some requests.
we populate Information message in "sap-message" which is available by default in Message Model. However to set the right target on the UI, how to relate these sap-message to the correct requests in UI.
below is the request and response.
One way is to populate the target with new create guid on the UI but as I understand, it is now allowed to pass Records GUID as target for this given scenario. Request your help on this.
Thanks
Arvind