1
0
mirror of https://github.com/sasjs/server.git synced 2026-07-23 21:25:29 +00:00

fix(api): return 200 with embedded log on SAS session failure instead of 400

A failed session (e.g. SAS %abort;, or a non-zero JS/PY/R exit) is a
normal outcome of running arbitrary user code, not a request-shape or
server problem. The #388 fix stopped processProgram() from hanging
forever on a failed SAS session, but did so by throwing a
SessionExecutionError, which surfaced as an HTTP 400 with a bespoke
JSON shape - breaking Studio's log tab, which only renders on 2xx.

Align the SAS branch with the pre-existing JS/PY/R behaviour: resolve
instead of throwing, and let Execution.ts fold session.failureReason
into the log the same way it already does for a debug-mode run. This
removes the now-dead SessionExecutionError class and its try/catch
wrapper entirely.

Update the diagrams in api/docs/diagrams/ to match.
This commit is contained in:
YuryShkoda
2026-07-15 13:59:01 +03:00
parent 9c3a1086b6
commit 850695024f
12 changed files with 128 additions and 102 deletions
@@ -2,7 +2,7 @@ import path from 'path'
import os from 'os'
import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils'
import * as ProcessProgramModule from '../processProgram'
import { ExecutionController, SessionExecutionError } from '../Execution'
import { ExecutionController } from '../Execution'
import { Session, SessionState, PreProgramVars } from '../../../types'
import { RunTimeType } from '../../../utils'
@@ -83,8 +83,15 @@ describe('ExecutionController.executeProgram', () => {
})
})
// A failed SAS session (e.g. from %abort;) is a normal outcome of
// running arbitrary user code, not a request-shape/server problem - the
// same way a plain SAS ERROR: in the log (without %abort;) already
// returns 200 with the log embedded. This mirrors the JS/PY/R failure
// path above: resolve normally, don't throw, and let the existing
// session.failureReason check below produce the same result shape as a
// successful run.
describe('SAS failure path', () => {
it('throws a SessionExecutionError carrying the complete log when the session fails', async () => {
it('returns the complete log embedded in a normal result instead of throwing, when the session fails', async () => {
const logPath = path.join(session.path, 'log.log')
const logContent =
'NOTE: SAS session\nERROR: SAS session terminated. See log for details.\n'
@@ -94,12 +101,17 @@ describe('ExecutionController.executeProgram', () => {
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => {
throw new Error('ERROR: SAS session terminated. See log for details.')
// mirrors the real SAS branch after the fix: sets
// state/failureReason and resolves, exactly like the JS/PY/R
// branch already does - it does not throw
session.state = SessionState.failed
session.failureReason =
'ERROR: SAS session terminated. See log for details.'
})
const controller = new ExecutionController()
const resultPromise = controller.executeProgram({
const { result } = await controller.executeProgram({
program: '%abort;',
preProgramVariables,
vars: {},
@@ -107,11 +119,8 @@ describe('ExecutionController.executeProgram', () => {
runTime: RunTimeType.SAS
})
await expect(resultPromise).rejects.toBeInstanceOf(SessionExecutionError)
await expect(resultPromise).rejects.toMatchObject({
log: logContent,
message: expect.stringContaining('SAS session terminated')
})
expect(session.state).toBe(SessionState.failed)
expect(result).toEqual(expect.stringContaining(logContent))
})
})
})