@sap/cds 9.8.3 → 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 CHANGED
@@ -4,6 +4,14 @@
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
+
7
15
  ## Version 9.8.3 - 2026-03-12
8
16
 
9
17
  ### Fixed
@@ -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) req._login = _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
- res.setHeader('Content-Type', isJson ? CT.JSON : CT.MULTIPART + ';boundary=' + boundary)
439
- res.status(200)
440
- res.write(isJson ? '{"responses":[' : '')
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
@@ -501,8 +516,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
501
516
  .catch(err => {
502
517
  responses._has_failure = true
503
518
 
504
- // abort batch on first failure with odata.continue-on-error: false
505
- 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) {
506
521
  _continue = false
507
522
  while (queue.length) queue.shift()()
508
523
  }
@@ -511,6 +526,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
511
526
  // here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
512
527
  _replaceResponsesWithCommitErrors(err, responses, ids)
513
528
  }
529
+
530
+ throw err
514
531
  })
515
532
  .finally(async () => {
516
533
  // trigger next in queue
@@ -524,6 +541,10 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
524
541
  for (let i = 0; i < agIndex; i++) prevs.push(promises[i])
525
542
  await Promise.allSettled(prevs)
526
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()
527
548
  if (isJson) _writeResponseJson(responses, res)
528
549
  else _writeResponseMultipart(responses, res, boundary)
529
550
  })
@@ -543,8 +564,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
543
564
 
544
565
  await Promise.allSettled(promises)
545
566
 
546
- res.write(isJson ? ']}' : `--${boundary}--${CRLF}`)
547
- res.end()
567
+ sendPostlude()
548
568
  } catch (e) {
549
569
  next(e)
550
570
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.8.3",
3
+ "version": "9.8.4",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [