Events and Requests
cds. context
This property provides seemingly static access to the current cds.EventContext
, that is, the current tenant
, user
, locale
, and so on, from wherever you are in your code. For example:
let { tenant, user } = cds.context
Usually that context is set by inbound middleware.
The property is realized as a so-called continuation-local variable, implemented using Node.js' async local storage technique, and a getter/setter pair: The getter is a shortcut forgetStore()
. The setter coerces values into valid instances of cds.EventContext
. For example:
[dev] cds repl
> cds.context = { tenant:'t1', user:'u2' }
> let ctx = cds.context
> ctx instanceof cds.EventContext //> true
> ctx.user instanceof cds.User //> true
> ctx.tenant === 't1' //> true
> ctx.user.id === 'u2' //> true
If a transaction object is assigned, its tx.context
is used, hence cds.context = tx
acts as a convenience shortcut for cds.context = tx.context
:
let tx = cds.context = cds.tx({ ... })
cds.context === tx.context //> true
TIP
Prefer local req
objects in your handlers for accessing event context properties, as each access to cds.context
happens through AsyncLocalStorage.getStore()
, which induces some minor overhead.
cds.EventContext
Instances of this class represent the invocation context of incoming requests and event messages, such as tenant
, user
, and locale
. Classes cds.Event
and cds.Request
inherit from it and hence provide access to the event context properties:
this.on ('*', req => {
let { tenant, user } = req
...
})
In addition, you can access the current event context from wherever you are in your code via the continuation-local variable cds.context
:
let { tenant, user } = cds.context
. http
If the inbound process came from an HTTP channel, you can now access express's common req
and res
objects through this property. It is propagated from cds.context
to all child requests, so Request.http
is accessible in all handlers including your database service ones like so:
this.on ('*', req => {
let { res } = req.http
res.set('Content-Type', 'text/plain')
res.send('Hello!')
})
Keep in mind that multiple requests (that is, instances of cds.Request
) may share the same incoming HTTP request and outgoing HTTP response (for example, in case of an OData batch request).
. id
A unique string used for request correlation.
For inbound HTTP requests the implementation fills it from these sources in order of precedence:
x-correlation-id
headerx-correlationid
headerx-request-id
headerx-vcap-request-id
header- a newly created UUID
On outgoing HTTP messages, it's propagated as x-correlation-id
header.
. locale
The current user's preferred locale, taken from the HTTP Accept-Language header of incoming requests and resolved to normalized.
. tenant
A unique string identifying the current tenant, or undefined
if not in multitenancy mode. In the case of multitenant operation, this string is used for tenant isolation, for example as keys in the database connection pools.
. timestamp
A constant timestamp for the current request being processed, as an instance of Date
. The CAP framework uses that to fill in values for the CDS pseudo variable $now
, with the guaranteed same value.
Learn more in the Managed Data guide.
. user
The current user, an instance of cds.User
as identified and verified by the authentication strategy. If no user is authenticated, cds.User.anonymous
is returned.
See reference docs for cds.User
.
TIP
Please note the difference between req
in a service handler (instance of cds.EventContext
) and req
in an express middleware (instance of http.IncomingMessage
). Case in point, req.user
in a service handler is an official API and, if not explicitely set, points to cds.context.user
. On the other hand, setting req.user
in a custom authentication middleware is deprecated.
cds.Event
Class cds.Event
represents event messages in asynchronous messaging, providing access to the event name, payload data, and optional headers. It also serves as the base class for cds.Request
and hence for all synchronous interactions.
. event
The name of the incoming event, which can be one of:
- The name of an incoming CRUD request like
CREATE
,READ
,UPDATE
,DELETE
- The name of a custom action or function like
submitOrder
- The name of a custom event like
OrderedBook
. data
Contains the event data. For example, the HTTP body for CREATE
or UPDATE
requests, or the payload of an asynchronous event message.
Use req.data
for modifications as shown in the following:
this.before ('UPDATE',Books, req => {
req.data.author = 'Schmidt'
req.query.UPDATE.data.author = 'Schmidt'
})
. headers
Provides access to headers of the event message or request. In the case of asynchronous event messages, it's the headers information sent by the event source. For HTTP requests it's the standard Node.js request headers.
eve. before 'commit'
eve. on 'succeeded'
eve. on 'failed'
eve. on 'done'
Register handlers to these events on a per event / request basis. The events are executed when the whole top-level request handling is finished
Use this method to register handlers, executed when the whole request is finished.
req.before('commit', () => {...}) // immediately before calling commit
req.on('succeeded', () => {...}) // request succeeded, after commit
req.on('failed', () => {...}) // request failed, after rollback
req.on('done', () => {...}) // request succeeded/failed, after all
DANGER
The events succeeded
, failed
, and done
are emitted after the current transaction ended. Hence, they run outside framework-managed transactions, and handlers can't veto the commit anymore.
To veto requests, either use the req.before('commit')
hook, or service-level before
COMMIT
handlers.
To do something that requires databases in succeeded
/failed
handlers, use cds.spawn()
, or one of the other options of manual transactions. Preferably use a variant with automatic commit/ rollback.
Example:
req.on('done', async () => {
await cds.tx(async () => {
await UPDATE `Stats` .set `views = views + 1` .where `book_ID = ${book.ID}`
})
})
Additional note about OData: For requests that are part of a changeset, the events are emitted once the entire changeset was completed. If at least one of the requests in the changeset fails, following the atomicity property ("all or nothing"), all requests fail.
cds.Request
Class cds.Request
extends cds.Event
with additional features to represent and deal with synchronous requests to services in event handlers, such as the query, additional request parameters, the authenticated user, and methods to send responses.
. method
The HTTP method of the incoming request:
msg.event | → | msg.method |
---|---|---|
CREATE | → | POST |
READ | → | GET |
UPDATE | → | PATCH |
DELETE | → | DELETE |
. target
Refers to the current request's target entity definition, if any; undefined
for unbound actions/functions and events. The returned definition is a linked definition as reflected from the CSN model.
For OData navigation requests along associations, msg.target
refers to the last target. For example:
OData Request | req.target |
---|---|
Books | AdminService.Books |
Books/201/author | AdminService.Authors |
Books(201)/author | AdminService.Authors |
See also req.path
to learn how to access full navigation paths.See Entity Definitions in the CSN reference.Learn more about linked models and definitions.
. path
Captures the full canonicalized path information of incoming requests with navigation. For requests without navigation, req.path
is identical to req.target.name
(or req.entity
, which is a shortcut for that).
Examples based on cap/samples/bookshop AdminService:
OData Request | req.path | req.target.name |
---|---|---|
Books | AdminService.Books | AdminService.Books |
Books/201/author | AdminService.Books/author | AdminService.Authors |
Books(201)/author | AdminService.Books/author | AdminService.Authors |
. entity
This is a convenience shortcut to msg.target.name
.
. params
Provides access to parameters in URL paths as an iterable with the contents matching the positional occurrence of parameters in the url path. The respective entry is the key value pair matching the entity definition.
For example, the parameters in an HTTP request like that:
GET /catalog/Authors(101)/books(title='Eleonora',edition=2) HTTP/1.1
The provided parameters can be accessed as follows:
const [ author, book ] = req.params
// > author === { ID: 101 }
// > book === { title: 'Eleonora', edition: 2 }
. query
Captures the incoming request as a CQN query. For example, an HTTP request like GET http://.../Books
is captured as follows:
req.query = {SELECT:{from:{ref:['Books']}}}
If bound custom operations req.query
contains the query to the entity, on which the bound custom operation is called. For unbound custom operations, req.query
contains an empty object.
. subject
Acts as a pointer to one or more instances targeted by the request. It can be used as input for cds.ql as follows:
SELECT.one.from(req.subject) //> returns single object
SELECT.from(req.subject) //> returns one or many in array
UPDATE(req.subject) //> updates one or many
DELETE(req.subject) //> deletes one or many
It's available for CRUD events and bound actions.
req. reply (results)
function req.reply (
results : object | object[] | string | number | true | false | null
)
Stores the given argument in req.results
, which is subsequently sent back to the client, rendered in a protocol-specific way.
this.on ('READ', Books, req => {
req.reply ([
{ ID: 1, title: 'Wuthering Heights' },
{ ID: 2, title: 'Catweazle' }
])
})
Alternatively, you can also just return a value from your .on
handler, which is then automatically used as the reply:
this.on ('READ', Books, req => {
return [
{ ID: 1, title: 'Wuthering Heights' },
{ ID: 2, title: 'Catweazle' }
]
})
req. reject ({ ... })
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.
this.on('CREATE', Books, req => {
const { title } = req.data
if (!title?.trim().length)
return req.reject ({
status: 400,
code: 'MISSING_INPUT',
message: 'Input is required',
target: 'title',
})
})
Best Practice: Use the @mandatory
annotation instead.
The sample above is just for illustration. Instead, use the @mandatory
annotation in your CDS model to define mandatory inputs like that:
entity Books {
key ID : Integer;
title : String(111) @mandatory;
...
}
This way, the framework automatically checks for mandatory inputs and rejects requests with errors if they are missing. So you don't have to (and should not) implement such checks manually in your code at all.
The basic variant used above accepts a single object as argument with these properties:
function req.reject ({
status? : number,
code? : string | number,
message? : string,
target? : string,
args? : string[],
... // custom properties
})
Property | Description |
---|---|
status | The numeric HTTP status code. |
code | A string code for clients to identify the error, also used as i18n key. |
message | A user-readable, potentially localized error message. |
target | The name of an input field/element an error is related to. |
args | Values to fill in to localized error messages. |
Learn more about target
for Fiori UIs
If status
is omitted, and code
is a number, that number is interpreted as the status code.
The code
is used as i18n key to lookup translations for error responses. If code
is omitted, a given message
will be used as i18n key.
req. reject ( ... )
This is a convenience variant of the req.reject()
method, with these arguments:
function req.reject (
code? : number,
message? : string,
target? : string,
args? : string[]
)
For example, it would allow rewriting the above sample like that:
this.on('CREATE', Books, req => {
const { title } = req.data
if (!title?.trim().length)
req.reject (400, 'MISSING_INPUT', 'title')
})
req. error()
Constructs and records an error with the given arguments. The method is similar to 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 subsequently.
For example:
req.error (400, 'Invalid input', 'some_field')
req.error (404, 'Not found')
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:
if (req.errors) ... //> errors occurred
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 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:
return req.reject ({
code: 'MULTIPLE_ERRORS',
details: [
{ status: 400, message: 'Invalid input', target: 'some_field' },
{ status: 404, message: 'Not found' }
]
})
req. warn()
req. info()
req. notify()
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.
req.notify ('Some notification message')
req.info ('Some information message')
req.warn ('Some warning message')
The methods are similar to req.error()
, also accepting the same arguments, 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.
Error Responses
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:
Status: 400
Content-Type: application/json
{
"error": {
"code": "MISSING_INPUT",
"message": "Input is required",
"target": "title"
}
}
OData error responses get cleansed
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.
Learn more about OData Error Responses
The error response is generated from the error object constructed via req.reject()
or req.error()
, and the properties are used and normalized as follows:
If
status
is given, it is used as the HTTP status code of the response. Ifstatus
is omitted, andcode
is a number in the range of 300...600, that number is used as the HTTP status code of the response.If
code
is given, and a string, it is used to look up a user-readable errormessage
from thei18n/messages
bundles. Ifcode
is omitted, the givenmessage
is used as the i18n key to look up themessage
, and if found, the original value ofmessage
is used ascode
in the response.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 themessage
property in the HTTP response. If no suitable localization is found, the original message as resolved in step 2 is returned.
For example:
req.reject ({ code: 400, message: 'MISSING_INPUT', target: 'title' })
req.reject (400, 'MISSING_INPUT', 'title') // same as above
... would result in a response like this for Accept-Language: de
:
Status: 400
Content-Type: application/json
{
"error": {
"code": "MISSING_INPUT",
"message": "Eingabe ist erforderlich",
"target": "title"
}
}
Error Sanitization
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
).
In very rare cases, you might want to return 5xx errors with a meaningful message to the client. This can be achieved with err.$sanitize = false
. Use that option with care!