@sap/cds 9.8.2 → 9.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/srv/protocols/hcql.js +1 -1
- package/libx/odata/middleware/batch.js +48 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 9.8.4 - 2026-03-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `res.statusCode` of batch sub-requests did not consider potential modifications during `srv.on('error')`
|
|
12
|
+
- Restore login challenge for late `401` with mocked authentication in `$batch`
|
|
13
|
+
- Batched request fails when depended upon atomicity group fails
|
|
14
|
+
|
|
15
|
+
## Version 9.8.3 - 2026-03-12
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- OData batch parallel processing: Drain remaining queue when aborting
|
|
20
|
+
|
|
7
21
|
## Version 9.8.2 - 2026-03-10
|
|
8
22
|
|
|
9
23
|
### Fixed
|
|
@@ -47,7 +47,7 @@ class HCQLAdapter extends require('./http') {
|
|
|
47
47
|
|
|
48
48
|
// Error formatting
|
|
49
49
|
router.use ((err, req, res, next) => {
|
|
50
|
-
err.$response = e => ({ errors: [ { ...e, message: e.message } ] })
|
|
50
|
+
if (typeof err === 'object') err.$response = e => ({ errors: [ { ...e, message: e.message } ] })
|
|
51
51
|
next(err)
|
|
52
52
|
})
|
|
53
53
|
|
|
@@ -149,7 +149,12 @@ const _createSubrequest = (request, _req, _res) => {
|
|
|
149
149
|
req.headers['content-type'] ??= _req.headers['content-type']
|
|
150
150
|
if (request.content_id) req.headers['content-id'] = request.content_id
|
|
151
151
|
req.body = request.body
|
|
152
|
-
if (_req._login)
|
|
152
|
+
if (_req._login) {
|
|
153
|
+
req._login = () => {
|
|
154
|
+
_req._login() //> sends 401 to client
|
|
155
|
+
req.res.sendStatus(401) //> fails the subrequest
|
|
156
|
+
}
|
|
157
|
+
}
|
|
153
158
|
// REVISIT: mark as subrequest (only needed for logging)
|
|
154
159
|
req._subrequest = true
|
|
155
160
|
|
|
@@ -392,6 +397,7 @@ const _replaceResponsesWithCommitErrors = (err, responses, ids) => {
|
|
|
392
397
|
const _serializeErrors = responses => {
|
|
393
398
|
for (const response of responses) {
|
|
394
399
|
if (response.status === 'fail' && typeof response.body === 'object') {
|
|
400
|
+
if (response.body.error?.status) response.statusCode = response.body.error.status
|
|
395
401
|
if (response.body.error && !response.body.error.toJSON) response.body.error = _getODataError(response.body.error)
|
|
396
402
|
response.body = JSON.stringify(response.body)
|
|
397
403
|
}
|
|
@@ -435,9 +441,18 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
435
441
|
if (only_gets && cds.env.odata.group_parallel_gets && _only_individual_requests())
|
|
436
442
|
atomicityGroups = [atomicityGroups.reduce((acc, cur) => (acc.push(...cur), acc), [])]
|
|
437
443
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
444
|
+
// IMPORTANT: Avoid sending headers and responses too eagerly, as we might still have to send a 401
|
|
445
|
+
let sendPostlude = () => {} //> only if prelude was sent
|
|
446
|
+
let sendPreludeOnce = () => {
|
|
447
|
+
res.setHeader('Content-Type', isJson ? CT.JSON : CT.MULTIPART + ';boundary=' + boundary)
|
|
448
|
+
res.status(200)
|
|
449
|
+
res.write(isJson ? '{"responses":[' : '')
|
|
450
|
+
sendPreludeOnce = () => {} //> only once
|
|
451
|
+
sendPostlude = () => {
|
|
452
|
+
res.write(isJson ? ']}' : `--${boundary}--${CRLF}`)
|
|
453
|
+
res.end()
|
|
454
|
+
}
|
|
455
|
+
}
|
|
441
456
|
|
|
442
457
|
const queue = []
|
|
443
458
|
let _continue = true
|
|
@@ -487,42 +502,49 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
487
502
|
|
|
488
503
|
// ensure all subrequests run in this tx
|
|
489
504
|
// (if first subrequest fails without really opening the tx, the rest are executed in a "dangling tx")
|
|
490
|
-
return Promise.allSettled(subrequests)
|
|
491
|
-
.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// throw first error and call srv.on('error') for the others
|
|
501
|
-
const first = failed.shift()
|
|
502
|
-
if (srv.handlers._error?.length)
|
|
503
|
-
for (const other of failed)
|
|
504
|
-
for (const each of srv.handlers._error) each.handler.call(srv, other.reason, cds.context)
|
|
505
|
-
throw first.reason
|
|
506
|
-
})
|
|
505
|
+
return Promise.allSettled(subrequests).then(ress => {
|
|
506
|
+
const failed = ress.filter(({ status }) => status === 'rejected')
|
|
507
|
+
if (!failed.length) return
|
|
508
|
+
// throw first error and call srv.on('error') for the others
|
|
509
|
+
const first = failed.shift()
|
|
510
|
+
if (srv.handlers._error?.length)
|
|
511
|
+
for (const other of failed)
|
|
512
|
+
for (const each of srv.handlers._error) each.handler.call(srv, other.reason, cds.context)
|
|
513
|
+
throw first.reason
|
|
514
|
+
})
|
|
507
515
|
})
|
|
508
516
|
.catch(err => {
|
|
509
517
|
responses._has_failure = true
|
|
510
518
|
|
|
511
|
-
// abort batch on first failure with odata.continue-on-error: false
|
|
512
|
-
if (!continue_on_error
|
|
519
|
+
// abort batch on first failure with odata.continue-on-error: false or if it was a 401
|
|
520
|
+
if (!continue_on_error || err.code == 401 || err.status == 401) {
|
|
521
|
+
_continue = false
|
|
522
|
+
while (queue.length) queue.shift()()
|
|
523
|
+
}
|
|
513
524
|
|
|
514
525
|
if (!responses.some(r => r.status === 'fail')) {
|
|
515
526
|
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
516
527
|
_replaceResponsesWithCommitErrors(err, responses, ids)
|
|
517
528
|
}
|
|
529
|
+
|
|
530
|
+
throw err
|
|
518
531
|
})
|
|
519
|
-
.finally(() => {
|
|
532
|
+
.finally(async () => {
|
|
520
533
|
// trigger next in queue
|
|
521
534
|
if (queue.length) queue.shift()()
|
|
522
535
|
|
|
523
536
|
// late error serialization
|
|
524
537
|
if (responses._has_failure) _serializeErrors(responses)
|
|
525
538
|
|
|
539
|
+
// wait for all previous atomicity groups (ignoring errors via allSettled) for odata v2
|
|
540
|
+
const prevs = []
|
|
541
|
+
for (let i = 0; i < agIndex; i++) prevs.push(promises[i])
|
|
542
|
+
await Promise.allSettled(prevs)
|
|
543
|
+
|
|
544
|
+
// don't write to res if already closed (e.g., due to 401 and login)
|
|
545
|
+
if (res.closed) return
|
|
546
|
+
|
|
547
|
+
sendPreludeOnce()
|
|
526
548
|
if (isJson) _writeResponseJson(responses, res)
|
|
527
549
|
else _writeResponseMultipart(responses, res, boundary)
|
|
528
550
|
})
|
|
@@ -540,10 +562,9 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
540
562
|
// trigger first max_parallel in queue
|
|
541
563
|
for (let i = 0; i < max_parallel; i++) if (queue.length) queue.shift()()
|
|
542
564
|
|
|
543
|
-
await Promise.
|
|
565
|
+
await Promise.allSettled(promises)
|
|
544
566
|
|
|
545
|
-
|
|
546
|
-
res.end()
|
|
567
|
+
sendPostlude()
|
|
547
568
|
} catch (e) {
|
|
548
569
|
next(e)
|
|
549
570
|
}
|