mirror of
https://github.com/sasjs/server.git
synced 2026-07-24 05:32:15 +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:
@@ -8,7 +8,7 @@ fresh interpreter process per request).
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["HTTP request<br/>POST /SASjsApi/code/execute<br/>or /SASjsApi/stp/execute"] --> B["Controller<br/>code.ts / stp.ts<br/>try/catch wrapper"]
|
||||
B --> C["ExecutionController.executeProgram<br/>or .executeFile<br/>Execution.ts:58-91"]
|
||||
B --> C["ExecutionController.executeProgram<br/>or .executeFile<br/>Execution.ts:40-77"]
|
||||
C --> D{"session already provided?<br/>(e.g. file-upload flow)"}
|
||||
D -- no --> E["getSessionController(runTime).getSession()<br/>Session.ts:59-69"]
|
||||
D -- yes --> F["use provided session"]
|
||||
@@ -18,30 +18,28 @@ flowchart TD
|
||||
H --> J["pre-warm: if pool has fewer<br/>than 3 pending, fire off more<br/>createSession() calls (not awaited)<br/>Session.ts:66"]
|
||||
I --> J
|
||||
F --> K
|
||||
J --> K["session.state = running<br/>Execution.ts:96"]
|
||||
K --> L["write webout.txt (empty),<br/>reqHeaders.txt<br/>Execution.ts:103-107"]
|
||||
L --> M["processProgram(...)<br/>processProgram.ts:16-27"]
|
||||
J --> K["session.state = running<br/>Execution.ts:78"]
|
||||
K --> L["write webout.txt (empty),<br/>reqHeaders.txt<br/>Execution.ts:85-89"]
|
||||
L --> M["processProgram(...)<br/>Execution.ts:96-107"]
|
||||
|
||||
M --> N{"runTime?"}
|
||||
|
||||
N -- SAS --> O["createSASProgram():<br/>wrap user code with _webout<br/>filename, macro vars, autoexec"]
|
||||
O --> P["write code.sas via .bkp + rename<br/>processProgram.ts:48-49"]
|
||||
P --> Q["poll: while session.state !== completed<br/>processProgram.ts:52-58"]
|
||||
Q -- "state becomes completed" --> R["resolve"]
|
||||
Q -- "state becomes failed" --> S["throw Error(session.failureReason)"]
|
||||
P --> Q["poll: while state !== completed<br/>AND state !== failed<br/>processProgram.ts:58-63"]
|
||||
Q --> R["processProgram() resolves either way -<br/>state was set by the session's own<br/>process exit handler in Session.ts,<br/>see sas-execution-handshake.md"]
|
||||
|
||||
N -- "JS / PY / R" --> T["createJSProgram / createPythonProgram /<br/>createRProgram: wrap user code similarly"]
|
||||
T --> U["write code file; spawn interpreter<br/>fresh via execFile; pipe stdout/stderr<br/>into a log.log write stream<br/>processProgram.ts:108-134"]
|
||||
U -- "exit 0" --> R
|
||||
U -- "exit non-zero" --> S
|
||||
T --> U["write code file; spawn interpreter<br/>fresh via execFile; pipe stdout/stderr<br/>into a log.log write stream<br/>processProgram.ts:117-124"]
|
||||
U -- "exit 0" --> Uc["session.state = completed<br/>processProgram.ts:126"]
|
||||
U -- "exit non-zero" --> Uf["session.state = failed<br/>session.failureReason = err.toString()<br/>processProgram.ts:131-133"]
|
||||
Uc --> R
|
||||
Uf --> R
|
||||
|
||||
R --> V["read log.log, webout.txt,<br/>stpsrv_header.txt<br/>Execution.ts:128-131"]
|
||||
V --> W["build httpHeaders + result<br/>return ExecuteReturnRaw<br/>Execution.ts:128-151"]
|
||||
W --> X["Controller: res.send(result)<br/>HTTP 200"]
|
||||
|
||||
S --> Y["catch in Execution.ts:109-126:<br/>read whatever log exists,<br/>throw SessionExecutionError(message, log)"]
|
||||
Y --> Z["Controller catch block<br/>rethrow { code: 400, status: 'failure',<br/>message, error, log }"]
|
||||
Z --> AA["res.status(err.code).send(err)<br/>HTTP 400 with complete log"]
|
||||
R --> V["read log.log, webout.txt,<br/>stpsrv_header.txt<br/>Execution.ts:109-113, 121-125"]
|
||||
V --> W["guard: if state !== failed,<br/>set state = completed<br/>(don't overwrite a failed session)<br/>Execution.ts:127-137"]
|
||||
W --> X["build httpHeaders + result;<br/>embed log in result if<br/>isDebugOn(vars) or<br/>session.failureReason is set<br/>Execution.ts:139-164"]
|
||||
X --> Y["Controller: res.send(result)<br/>HTTP 200<br/>(log embedded if the session failed -<br/>same shape as a successful run)"]
|
||||
```
|
||||
|
||||
## Notes
|
||||
@@ -55,9 +53,16 @@ flowchart TD
|
||||
`failed` either way, and both paths flow through the same log/webout
|
||||
reading logic in `Execution.ts` afterward. The mechanics of *how* that
|
||||
state gets set differ substantially (see `session-lifecycle.md`).
|
||||
- **A failed session never throws.** `processProgram()` resolves normally
|
||||
whether the session completed or failed - 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/server problem. There is no
|
||||
separate error path: the same `Execution.ts:139-164` logic that embeds
|
||||
the log for a debug-mode successful run also embeds it when
|
||||
`session.failureReason` is set, and the controller always responds 200.
|
||||
- **`includePrintOutput`** (SAS only) additionally appends `output.lst`
|
||||
content to the result when debug mode is on - omitted above for brevity;
|
||||
see `Execution.ts:135-142`.
|
||||
see `Execution.ts:149-156`.
|
||||
- **`triggerProgram`/`triggerCode`** (fire-and-forget variants, not shown)
|
||||
call the same `ExecutionController` methods without awaiting them and
|
||||
immediately return `{ sessionId }`; the client polls
|
||||
|
||||
Reference in New Issue
Block a user