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.contextUsually 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' //> trueIf 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 //> trueTIP
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-idheaderx-correlationidheaderx-request-idheaderx-vcap-request-idheader- 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 allDANGER
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.1The 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']}}}For bound custom operations, req.query contains the query to the entity on which the operation is called. For unbound custom operations, req.query contains an empty object.
. subject
Acts as a pointer to the instances targeted by the request. For example for the equivalents of inbound requests addressing single rows like these:
AdminService.read(Books,201)
AdminService.update(Books,201).with({...})
AdminService.delete(Books,201)... req.subject would always look like that:
req.subject //> ...
{ ref: [{
id: 'AdminService.Books', // == req.target.name
where: [ { ref: [ 'ID' ] }, '=', { val: 201 } ]
}]}... which allows it to be used in custom handlers of each inbound request to easily read or write this very target row using cds.ql as follows:
SELECT.from(req.subject) //> returns the single target row
UPDATE(req.subject)... //> updates the single target row
DELETE.from(req.subject) //> deletes the single target rowWARNING
You can use req.subject in custom handlers for inbound READ, UPDATE and DELETE requests, as well as in bound actions, addressing single rows. You can't use it reasonably in custom handlers for INSERT requests or other requests addressing multiple row.
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 (
status? : 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 occurredAfter 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.
User Input & Injection Vulnerabilities
Ensure proper validation of the message text if it contains values from user input.
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
statusis given, it is used as the HTTP status code of the response. Ifstatusis omitted, andcodeis a number in the range of 300...600, that number is used as the HTTP status code of the response.If
codeis given, and a string, it is used to look up a user-readable errormessagefrom thei18n/messagesbundles. Ifcodeis omitted, the givenmessageis used as the i18n key to look up themessage, and if found, the original value ofmessageis used ascodein the response.If an
Accept-Languageheader is present in the request, a localized message is looked up in addition, using the preferred language specified in the header, and used for themessageproperty 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!
Translations for Validation Errors
For the following annotations/error codes, the runtime provides default translations:
| Annotation | Error Code |
|---|---|
@mandatory | ASSERT_MANDATORY(1) |
@assert.range | ASSERT_RANGE |
@assert.range on enum | ASSERT_ENUM |
@assert.format | ASSERT_FORMAT |
@assert.target | ASSERT_TARGET |
(1) Falls back to error code ASSERT_NOT_NULL if provided in custom translations.
These can be overridden by the known technique of providing custom i18n messages.