Skip to content

Commit a16f555

Browse files
committed
Revamped docs for Error Handling in Node.js
1 parent 97c12ae commit a16f555

2 files changed

Lines changed: 212 additions & 74 deletions

File tree

node.js/_menu.md

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
# [The cds Facade](cds-facade)
22

3-
# [cds. Services](core-services)
4-
5-
## [Class cds. Service](core-services)
6-
## [Class cds. ApplicationService](app-services)
7-
## [Class cds. RemoteService](remote-services)
8-
## [Class cds. MessagingService](messaging)
9-
## [Class cds. DatabaseService](databases)
10-
## [Class cds. SQLService](databases)
11-
12-
# [cds. Events](events)
13-
14-
## [cds. context](events#cds-context)
15-
## [Class cds. EventContext](events#cds-event-context)
16-
## [Class cds. Event](events#cds-event)
17-
## [Class cds. Request](events#cds-request)
18-
193
# [cds. compile()](cds-compile)
204

215
## [cds. compile()](cds-compile#cds-compile)
@@ -36,11 +20,29 @@
3620
## [Class cds. Association](cds-reflect#cds-association)
3721
## [cds. linked. classes](cds-reflect#cds-linked-classes)
3822

39-
# [cds. server()](cds-server)
23+
# [cds. server](cds-server)
4024
# [cds. serve()](cds-serve)
4125
# [cds. connect()](cds-connect)
4226

43-
# [cds. ql](cds-ql)
27+
# [cds. Services](core-services)
28+
29+
## [Class cds. Service](core-services)
30+
## [Class cds. ApplicationService](app-services)
31+
## [Class cds. RemoteService](remote-services)
32+
## [Class cds. MessagingService](messaging)
33+
## [Class cds. DatabaseService](databases)
34+
## [Serving Fiori UIs](fiori)
35+
36+
# [cds. Events](events)
37+
38+
## [cds. context](events#cds-context)
39+
## [Class cds. EventContext](events#cds-event-context)
40+
## [Class cds. Event](events#cds-event)
41+
## [Class cds. Request](events#cds-request)
42+
## [Error Handling](events#req-reject)
43+
## [Event Queues](queue)
44+
45+
# [cds. Queries](cds-ql)
4446

4547
## [SELECT](cds-ql#select)
4648
## [INSERT](cds-ql#insert)
@@ -49,15 +51,14 @@
4951
## [DELETE](cds-ql#delete)
5052
## [Expressions](cds-ql#expressions)
5153

52-
# [cds. tx()](cds-tx)
5354
# [cds. log()](cds-log)
54-
# [cds. env](cds-env)
55-
# [cds. auth](authentication)
5655
# [cds. i18n](cds-i18n)
56+
# [cds. env](cds-env)
5757
# [cds. utils](cds-utils)
58-
# [cds. test()](cds-test)
59-
# [cds. plugins](cds-plugins)
60-
# [cds. queued()](queue)
58+
59+
# [Transactions](cds-tx)
60+
# [Security](authentication)
61+
# [Plugins](cds-plugins)
62+
# [Testing](cds-test)
6163
# [TypeScript](typescript)
62-
# [Fiori Support](fiori)
6364
# [Best Practices](best-practices)

node.js/events.md

Lines changed: 186 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -361,93 +361,230 @@ It's available for CRUD events and bound actions.
361361

362362

363363

364-
### req. reply() {.method}
365-
[`req.reply`]: #req-reply
366364

367-
Stores the given `results` in `req.results`, which is then sent back to the client, rendered in a protocol-specific way.
365+
### req. reply (results) {.method}
368366

367+
```tsx
368+
function req.reply (
369+
results : object | object[] | string | number | true | false | null
370+
)
371+
```
369372

373+
Stores the given argument in `req.results`, which is subsequently sent back to the client, rendered in a protocol-specific way.
370374

371-
### req. reject() {.method}
372-
[`req.reject`]: #req-reject
375+
```js
376+
this.on ('READ', Books, req => {
377+
req.reply ([
378+
{ ID: 1, title: 'Wuthering Heights' },
379+
{ ID: 2, title: 'Catweazle' }
380+
])
381+
})
382+
```
373383

374-
Rejects the request with the given HTTP response code and single message. Additionally, `req.reject` throws an error based on the passed arguments. Hence, no additional code and handlers is executed once `req.reject` has been invoked.
384+
Alternatively, you can also just return a value from your `.on` handler, which is then automatically used as the reply:
375385

376-
[Arguments are the same as for `req.error`](#req-error){.learn-more}
386+
```js
387+
this.on ('READ', Books, req => {
388+
return [
389+
{ ID: 1, title: 'Wuthering Heights' },
390+
{ ID: 2, title: 'Catweazle' }
391+
]
392+
})
393+
```
377394

378395

396+
### req. reject ({ ... }) {.method #req-reject}
379397

380398

381-
### req. error() {.method}
382-
### req. warn() {.method}
383-
### req. info() {.method}
384-
### req. notify() {.method}
399+
Constructs and throws an error with the given arguments, which is then sent back to the client in an error response. This is the preferred way to reject requests with errors.
385400

386-
[`req.info`]: #req-msg
387-
[`req.error`]: #req-msg
401+
```js
402+
this.on('CREATE', Books, req => {
403+
const { title } = req.data
404+
if (!title?.trim().length)
405+
return req.reject ({ // [!code focus]
406+
status: 400, // [!code focus]
407+
code: 'MISSING_INPUT', // [!code focus]
408+
message: 'Input is required', // [!code focus]
409+
target: 'title', // [!code focus]
410+
}) // [!code focus]
411+
})
412+
```
388413

389-
Use these methods to collect messages or errors and return them in the request response to the caller. The method variants reflect different severity levels. Use them as follows:
414+
::: details **Best Practice:**{.good} Use the `@mandatory` annotation instead.
415+
The sample above is just for illustration. Instead, use the [`@mandatory`](../guides/providing-services.md#mandatory)
416+
annotation in your CDS model to define mandatory inputs like that:
390417

391-
#### <i> Variants </i>
418+
```cds
419+
entity Books {
420+
key ID : Integer;
421+
title : String(111) @mandatory; // [!code focus]
422+
...
423+
}
424+
```
392425

393-
| Method | Collected in | Typical UI | Severity |
394-
| -------------- | -------------- | ---------- | :------: |
395-
| `req.notify()` | `req.messages` | Toasters | 1 |
396-
| `req.info()` | `req.messages` | Dialog | 2 |
397-
| `req.warn()` | `req.messages` | Dialog | 3 |
398-
| `req.error()` | `req.errors` | Dialog | 4 |
426+
This way, the framework automatically checks for mandatory inputs and rejects requests with errors if they are missing.
427+
So you don't have to (and should not) implement such checks manually in your code at all.
428+
:::
399429

400-
{style="font-style:italic;width:80%;"}
401430

402-
**Note:** messages with a severity less than 4 are collected and accessible in property `req.messages`, while error messages are collected in property `req.errors`. The latter allows to easily check, whether errors occurred with:
431+
The basic variant used above accepts a single object as argument with these properties:
432+
433+
```tsx
434+
function req.reject ({
435+
status? : number,
436+
code? : string | number,
437+
message? : string,
438+
target? : string,
439+
args? : string[],
440+
... // custom properties
441+
})
442+
```
443+
444+
| Property | Description |
445+
| -------- | ----------- |
446+
| `status` | The numeric [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). |
447+
| `code` | A string code for clients to identify the error, also used as [i18n](cds-i18n) key. |
448+
| `message`| A user-readable, potentially localized error message. |
449+
| `target` | The name of an input field/element an error is related to. |
450+
| `args` | Values to fill in to localized error messages. |
451+
452+
[Learn more about `target` for Fiori UIs](https://ui5.sap.com/#/topic/fbe1cb5613cf4a40a841750bf813238e){.learn-more}
453+
454+
455+
If `status` is omitted, and `code` is a number, that number is interpreted as the status code.
456+
457+
The `code` is used as [i18n](cds-i18n) key to lookup translations for [error responses](#error-responses). If `code` is omitted, a given `message` will be used as [i18n](cds-i18n) key.
458+
459+
460+
### req. reject ( ... ) {.method}
461+
462+
This is a convenience variant of the [`req.reject()`](#req-reject) method, with these arguments:
463+
464+
```tsx
465+
function req.reject (
466+
code? : number,
467+
message? : string,
468+
target? : string,
469+
args? : string[]
470+
)
471+
```
472+
473+
For example, it would allow rewriting the [above](#req-reject) sample like that:
403474

404475
```js
405-
if (req.errors) //> get out somehow...
476+
this.on('CREATE', Books, req => {
477+
const { title } = req.data
478+
if (!title?.trim().length)
479+
req.reject (400, 'MISSING_INPUT', 'title') // [!code focus]
480+
})
406481
```
407482

408483

409-
#### <i> Arguments </i>
410484

411-
- `code` _Number (Optional)_ - Represents the error code associated with the message. If the number is in the range of HTTP status codes and the error has a severity of 4, this argument sets the HTTP response status code.
412-
- `message` _String \| Object \| Error_ - See below for details on the non-string version.
413-
- `target` _String (Optional)_ - The name of an input field/element a message is related to.
414-
- `args` _Array (Optional)_ - Array of placeholder values. See [Localized Messages](cds-i18n) for details.
415485

416-
::: tip `target` property for UI5 OData model
417-
The `target` property is evaluated by the UI5 OData model and needs to be set according to [Server Messages in the OData V4 Model](https://ui5.sap.com/#/topic/fbe1cb5613cf4a40a841750bf813238e).
418-
:::
419486

420487

421-
#### <i> Using an Object as Argument </i>
488+
### req. error() {.method}
489+
490+
Constructs and records an error with the given arguments. The method is similar to [`req.reject()`](#req-reject), and accepts the same arguments, but does not throw the error immediately. Instead, it collects errors in `req.errors`, which are sent back to the client in an [error response](#error-responses) subsequently.
491+
492+
For example:
493+
494+
```js
495+
req.error (400, 'Invalid input', 'some_field')
496+
req.error (404, 'Not found')
497+
```
498+
499+
All errors are collected in property `req.errors`, which is initially `undefined`, and initialized as an array on the first call. This allows to easily check, whether errors occurred with:
500+
501+
```js
502+
if (req.errors) ... //> errors occurred
503+
```
422504

423-
You can also pass an object as the sole argument, which then contains the properties `code`, `message`, `target`, and `args`. Additional properties are preserved until the error or message is sanitized for the client. In case of an error, the additional property `status` can be used to specify the HTTP status code of the response.
505+
After each phase of request processing, i.e. _before_ / _on_ / _after_, the framework checks whether errors got recorded in `req.errors`. If so, it automatically [rejects](#req-reject) the request with an aggregate error containing all recorded errors, and the request is not processed further. So, in essence, the above ends up in the equivalent of:
424506

425507
```js
426-
req.error ({
427-
code: 'Some-Custom-Code',
428-
message: 'Some Custom Error Message',
429-
target: 'some_field',
430-
status: 418
508+
return req.reject ({
509+
code: 'MULTIPLE_ERRORS',
510+
details: [
511+
{ status: 400, message: 'Invalid input', target: 'some_field' },
512+
{ status: 404, message: 'Not found' }
513+
]
431514
})
432515
```
433516

434-
Additional properties can be added as well, for example to be used in [custom error handlers](core-services#srv-on-error).
435517

436-
> In OData responses, notifications get collected and put into HTTP response header `sap-messages` as a stringified array, while the others are collected in the respective response body properties (&rarr; see [OData Error Responses](https://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091)).
518+
### req. warn() {.method}
519+
### req. info() {.method}
520+
### req. notify() {.method}
437521

438-
#### <i> Error Sanitization </i>
522+
Use these methods to record messages to be sent back to the client not in an error response but in addition to a successful response.
439523

440-
In production, errors should never disclose any internal information that could be used by malicious actors. Hence, we sanitize all server-side errors thrown by the CAP framework. That is, all errors with a 5xx status code (the default status code is 500) are returned to the client with only the respective generic message (example: `500 Internal Server Error`). Errors defined by app developers aren't sanitized and returned to the client unchanged.
524+
```js
525+
req.notify ('Some notification message')
526+
req.info ('Some information message')
527+
req.warn ('Some warning message')
528+
```
529+
530+
The methods are similar to [`req.error()`](#req-error), also accepting the [same arguments](#req-reject), but the messages are collected in `req.messages` instead of `req.errors`, not decorated with stack traces, and returned in a HTTP response header (e.g. `sap-messages`), instead of the response body.
441531

442-
Additionally, the OData protocol specifies which properties an error object may have. If a custom property shall reach the client, it must be prefixed with `@` to not be purged.
443532

444533

445-
### req. diff() <Beta /> {.method}
446-
[`req.diff`]: #req-diff
534+
## Error Responses
447535

448-
Use this asynchronous method to calculate the difference between the data on the database and the passed data (defaults to `req.data`, if not passed). Note that the usage of `req.diff` only makes sense in *before* handlers as they are run before the actual change was persisted on the database.
449-
> This triggers database requests.
536+
When a request is rejected with an error, the protocol adapters provided with the CAP framework automatically renders them in a protocol-specific way, for example, like that in case of _OData_ as well as _REST_ endpoints:
537+
538+
```http
539+
Status: 400
540+
Content-Type: application/json
541+
542+
{
543+
"error": {
544+
"code": "MISSING_INPUT",
545+
"message": "Input is required",
546+
"target": "title"
547+
}
548+
}
549+
```
550+
551+
::: details OData error responses get cleansed
552+
553+
In order to be compliant with the spec, all custom properties not foreseen in the spec are purged from the error response. If a custom property shall reach the client, it must be prefixed with `@` to not be purged.
554+
555+
:::
556+
557+
[Learn more about OData Error Responses](https://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091){.learn-more}
558+
559+
The error response is generated from the error object constructed via [`req.reject()`](#req-reject) or [`req.error()`](#req-error), and the properties are used and normalized as follows:
560+
561+
1. If `status` is given, it is used as the HTTP status code of the response. If `status` is omitted, and `code` is a number in the range of 300...600, that number is used as the HTTP status code of the response.
562+
563+
2. If `code` is given, and a string, it is used to look up a user-readable error `message` from the [`i18n/messages`](cds-i18n) bundles. If `code` is omitted, the given `message` is used as the [i18n](cds-i18n) key to look up the `message`, and if found, the original value of `message` is used as `code` in the response.
564+
565+
3. If an `Accept-Language` header is present in the request, a localized message is looked up in addition, using the preferred language specified in the header, and used for the `message` property in the HTTP response. If no suitable localization is found, the original message as resolved in step 2 is returned.
566+
567+
For example:
450568

451569
```js
452-
const diff = await req.diff()
570+
req.reject ({ code: 400, message: 'MISSING_INPUT', target: 'title' })
571+
req.reject (400, 'MISSING_INPUT', 'title') // same as above
572+
```
573+
574+
... would result in a response like this for `Accept-Language: de`:
575+
576+
```http
577+
Status: 400
578+
Content-Type: application/json
579+
580+
{
581+
"error": {
582+
"code": "MISSING_INPUT",
583+
"message": "Eingabe ist erforderlich",
584+
"target": "title"
585+
}
586+
}
453587
```
588+
589+
> [!warning] Error Sanitization
590+
> In production, error responses should never disclose internal information that could be exploited by attackers. To ensure that, all errors with a `5xx` status code are returned to the client with only the respective generic message (example: `500 Internal Server Error`).

0 commit comments

Comments
 (0)