sap-for-agents
Health Pass
- License — License: NOASSERTION
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 13 GitHub stars
Code Fail
- rm -rf — Recursive force deletion command in start.sh
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
SAP NWRFC → REST gateway designed for AI agents: search, inspect, invoke RFC/BAPI, read tables, triage ST22 dumps, and edit ABAP code over plain HTTP
sap-for-agents
Wraps the SAP NWRFC SDK into a long-running HTTP service that exposes any SAP RFC / BAPI through a RESTful interface. Other services can invoke SAP without installing the SDK — a single JSON POST does the job.
- Stack: Rust (standard-library FFI linking directly to
sapnwrfc.dll) + axum + tokio + serde - Zero-SDK clients: callers only need to send an HTTP POST
- Generic interface: one endpoint,
/api/rfc, describes any BAPI — no per-BAPI glue code - AI-friendly: 15 metadata endpoints (search functions / inspect interfaces / read docs / view source / read transparent tables / query the data dictionary / triage short dumps / edit ABAP code) let agents explore and act self-service. The operator guide for AI lives in
AGENTS.md
⚠️ Risk disclaimer: this is an exploratory, experimental project, primarily built for learning, testing, and local development scenarios. It has not been hardened for production use, offers no guarantee of stability or correctness, and its APIs may change at any time. It grants RFC access with the full privileges of the configured
SAP_USER— before using it, you must evaluate the risks yourself (data exposure, unauthorized calls, compliance, etc.) and take your own precautions. Use it against production SAP systems at your own risk; the authors accept no liability for any loss arising from its use.
Table of Contents
- Quick Start
- §1 Use Cases and Limitations
- §2 Configuration Reference
- §3 API Reference — incl. 3.3 AI-facing Metadata API
- §4 Call Examples
- §5 Common BAPI Quick Reference
- §6 Error Handling
- §7 Deployment Tips
- §8 Architecture and Limitations
- §9 Server Mode (Called by SAP) — see docs/SERVER_MODE.md
- §10 Releasing a New Version (Maintainers)
- License
Quick Start
You need both of the following:
- Prebuilt binary / Rust source: provides the HTTP service and the SAP protocol binding code
- SAP NWRFC SDK: SAP's proprietary C library that provides the actual SAP communication implementation (cannot be redistributed with this project due to licensing)
A prebuilt binary saves you from installing the Rust toolchain and a ~23-second compile, but it does not save you from the SDK — the SAP library is still linked at runtime.
📦 The easy way: drop the matching-platform zip you downloaded from SAP (e.g.
nwrfcsdk-...-darwin-arm64.zipon macOS,...-linux-x86_64.zipon Linux) into any subdirectory undernwrfcsdk/lib/. The startup script auto-extracts it to the correct<os>-<arch>/path. See "Auto-install the SDK" below.⚠️ The zip must match your current platform (
.dylib↔macOS,.so↔Linux,.dll↔Windows). The script does not validate the platform; placing a zip for the wrong platform leaves library files that cannot be loaded after extraction.
Auto-install the SDK (recommended)
start.sh / start.ps1 look for the SDK in this order:
- Environment variable
SAP_SDK_DIR— points to an already-installed SDK root (most flexible; common for Docker/CI) nwrfcsdk/lib/<os>-<arch>/— the default path with library files already in placenwrfcsdk/lib/<any>/nwrfcsdk-*.zip— auto-detected and extracted to the correct path ✨- None found → errors out with clear guidance
Easiest: drop the whole matching-platform zip you downloaded from SAP into any subdirectory under nwrfcsdk/lib/ and let the startup script handle it. Example (macOS Apple Silicon):
nwrfcsdk/
└── lib/
└── incoming/ ← create any directory
└── nwrfcsdk-...-darwin-arm64.zip ← must be the SDK for the current platform
Then run ./start.sh. The script automatically:
- Extracts the zip
- Locates the library files inside the zip (SAP SDK zips are typically
nwrfcsdk/lib/<file>with no platform subdirectory) - Copies them to
nwrfcsdk/lib/darwin-aarch64/(or the corresponding platform subdirectory) - Cleans up temporary files
After extraction the real path still follows SAP's official layout, which makes future SDK updates easier.
Download a prebuilt binary (recommended for end users)
Skip Rust; use a ready-made binary:
- Open GitHub Releases → pick the latest tag
- Download the archive for your platform:
- Linux x86_64:
sap-for-agents-x86_64-unknown-linux-gnu.tar.gz - Linux ARM64:
sap-for-agents-aarch64-unknown-linux-gnu.tar.gz - macOS Intel:
sap-for-agents-x86_64-apple-darwin.tar.gz - macOS Apple Silicon:
sap-for-agents-aarch64-apple-darwin.tar.gz - Windows x86_64:
sap-for-agents-x86_64-pc-windows-msvc.zip
- Linux x86_64:
- Extract it; inside you'll find
sap_for_agents(or.exe) +README.md+.env.example+ thenwrfcsdk/directory skeleton - Download the SAP NWRFC SDK: register an account on the SAP Support Portal (requires SAP customer/partner status), search for
SAP NW RFC SDK, and download the zip for your platform - Place the zip into any subdirectory under
nwrfcsdk/lib/(e.g.nwrfcsdk/lib/incoming/); the startup script auto-extracts it to the correct<os>-<arch>/path. The zip must match your current platform (macOS→.dylib, Linux→.so, Windows→.dll) cp .env.example .envand fill in the SAP connection parameters- Run:
- Linux/macOS:
./sap_for_agents - Windows: double-click
sap_for_agents.exeor launch it from PowerShell
- Linux/macOS:
Windows users: CI now builds a Windows x86_64 binary automatically (a
.deffile generates a stub import library to work around the MSVC linking restriction). After extracting the zip you still need to placesapnwrfc.dllyourself; see steps 4–5.
Option 1: Run locally (development/debugging)
# 1. Install Rust (skip if already installed)
# Windows: winget install Rustlang.Rustup
# Linux/macOS: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Place the SAP SDK (matching platform subdirectory; see nwrfcsdk/README.md)
# Windows: nwrfcsdk/lib/windows-x86_64/ ← place sapnwrfc.dll, etc.
# Linux: nwrfcsdk/lib/linux-x86_64/ ← place libsapnwrfc.so, etc.
# 3. Configure connection settings
cp .env.example .env # then edit .env and fill in SAP connection params
# 4. One-shot environment check and launch (start.ps1 on Windows, start.sh on Linux/macOS)
./start.sh # or: powershell -File start.ps1
# 5. Verify (in a new terminal)
curl http://127.0.0.1:3000/health
# → {"status":"ok"}
Option 2: Docker (deployment)
# 1. Place the Linux SDK into nwrfcsdk/lib/linux-x86_64/ (needed at build time)
# 2. Copy and fill in the configuration
cp .env.example .env # edit: fill in SAP connection params + SAP_SDK_HOST_PATH (below)
# 3. One-shot up (docker compose auto-builds + runs + mounts the SDK + injects config)
docker compose up -d --build
# 4. Verify
curl http://127.0.0.1:3000/health
Add one extra entry to .env: SAP_SDK_HOST_PATH — the absolute path of the Linux SDK directory on the host (e.g. C:\Users\you\sap-sdk or /opt/sap/nwrfcsdk). compose mounts it into the container at /app/nwrfcsdk. This directory's layout must match nwrfcsdk/ (containing lib/linux-x86_64/libsapnwrfc.so).
First call
Once the service is up, any HTTP client can call it:
curl -X POST http://127.0.0.1:3000/api/rfc \
-H "Content-Type: application/json" \
-d '{"func_name":"STFC_CONNECTION","inputs":{"REQUTEXT":"hello"},"string_outputs":{"ECHOTEXT":{"max_len":null}}}'
Leave
max_lenasnull— the service auto-discovers the field length from SAP metadata, so you don't have to fill it in by hand.
1. Use Cases and Limitations
Good fit
- Microservice architectures that let Python / Node / Java services call SAP over HTTP
- Automation scripts, ETL, reporting backends
- Local development and debugging of BAPIs
Capability boundaries
| Capability | Support |
|---|---|
| Connection | ✅ Direct connect (ASHOST), auto-reconnect, graceful shutdown |
| Scalar input | ✅ string/int/float auto-dispatched by JSON type; BCD/INT8/binary via explicit {"type":"...","value":...} |
| Scalar output | ✅ int_outputs (read as INT) / string_outputs (read as string, length auto-discoverable) / auto_outputs (read by the metadata's true type, preserving INT/FLOAT/INT8/binary semantics) |
| Table parameters (TABLES) | ✅ multi-row input + output traversal; fields with "auto":true are read by true type (INT/FLOAT/INT8/Base64), otherwise as string |
| Top-level structure parameters | ✅ struct_inputs / struct_outputs (e.g. BAPI_USER_CREATE.ADDRESS); output fields also support auto for true-type reading |
| BCD/INT8/binary input | ✅ {"type":"BCD",...} / {"type":"INT8",...} / {"type":"BYTES",...} (BYTES as Base64) |
| Metadata auto-discovery | ✅ field-length caching, no manual max_len needed (works for scalar/table/structure outputs) |
| Server mode (called back by SAP) | ✅ config-driven webhook forwarding (SAP_ROLE=server); see §9 |
| tRFC/qRFC/bgRFC | ❌ Not supported |
| SSO/SNC secure logon | ❌ Username/password only |
Other limitations
| Item | Notes |
|---|---|
| Concurrency | Multi-connection pool (default 8, configurable via SAP_POOL_SIZE); SAP calls from different requests run in parallel; when the pool is exhausted, acquire waits up to 120s |
| Character set | Bridges SAP UC via UTF-16; UTF-8 input and output |
| Platforms | Windows/Linux/macOS × x86_64/aarch64 (build.rs auto-selects the SDK subdirectory) |
| RFC call timeout | The connection-pool layer has an acquire timeout (120s); a single RFC call has no execution timeout yet |
2. Configuration Reference
All configuration goes through environment variables, written to .env in the project root (gitignored, never committed). Quick Start covers the basics; this section is the full field reference.
| Variable | Required | Default | Description |
|---|---|---|---|
SAP_ASHOST |
✅ | — | SAP application server hostname/IP |
SAP_SYSNR |
✅ | — | System number, e.g. 00 |
SAP_CLIENT |
✅ | — | Client number, e.g. 001 |
SAP_USER |
✅ | — | Logon account |
SAP_PASSWD |
✅ | — | Logon password |
SAP_LANG |
❌ | EN |
Logon language (also sets the default language for doc endpoints) |
SAP_LISTEN_ADDR |
❌ | 127.0.0.1:3000 |
HTTP service listen address |
SAP_POOL_SIZE |
❌ | 8 |
SAP connection pool cap (number of concurrent calls), ≥1 |
SAP_REQUEST_TIMEOUT_SECS |
❌ | 60 |
Global timeout in seconds for a single SAP call, ≥1; returns 504 on timeout. /api/rfc accepts a per-request timeout_secs in the body to override it |
SAP_RATE_LIMIT_RPS |
❌ | (no rate limit) | Requests per second per caller IP for /api; set ≥1 to enable; returns 429 when exceeded |
SAP_ROLE |
❌ | client |
Run mode: client/server/both (server mode: see §9) |
SAP_SDK_DIR |
❌ | ./nwrfcsdk |
SDK root directory (for Docker/CI/custom paths) |
Production deployment tip: do not bake
.envinto the image layer. Use your orchestration system's secret injection (K8s Secret / Docker Swarm secret) instead.
3. API Reference
Authentication (optional)
Once SAP_API_KEY is set, every /api/* business endpoint requires the request header Authorization: Bearer <token>; without it the service is unauthenticated (the localhost default). The probes /health, /ready, and the public pages /, /agents.md are always open.
# Enable authentication (generate a long random string)
export SAP_API_KEY=$(openssl rand -hex 32)
# Send the token when calling
curl -H "Authorization: Bearer $SAP_API_KEY" \
http://127.0.0.1:3000/api/functions/BAPI_USER_GETLIST
⚠️ As soon as you expose the service to a network (
SAP_LISTEN_ADDR=0.0.0.0or Docker deployment), always setSAP_API_KEY. Otherwise anyone who can reach the port can invoke any RFC with the privileges ofSAP_USER. Failure returns401 {"code":401,"message":"..."}+WWW-Authenticate: Bearer.
3.1 Liveness and readiness probes
Two probe endpoints with distinct semantics:
GET /health — liveness (process alive)
Does not touch SAP; returns instantly; used to check whether the process is alive.
{ "status": "ok" }
GET /ready — readiness (SAP reachable)
Borrows a connection from the pool and calls the SAP standard function RFC_PING (with a 5s timeout) to verify the backend is reachable.
- Success:
200 { "status": "ready", "sap": "ok" } - SAP unreachable / timeout:
503 { "status": "unavailable" | "timeout", ... }
For orchestration systems (K8s, etc.): use /health as the livenessProbe (restart only when the process dies) and /ready as the readinessProbe (only shed traffic and wait for recovery when SAP is unreachable).
GET /metrics — Prometheus metrics (unauthenticated)
Returns Prometheus text-format metrics for Prometheus / Grafana and other scrapers:
pool_idle/pool_total/pool_max— connection pool idle / total built / caprfc_calls_total{func,result}— RFC call count (by function × success/failure)rfc_call_duration_ms{func}— call-duration histogram (with p50/p90/p99)
Unauthenticated (an ops probe, like
/healthand/ready). On a public deployment, protect it at the reverse-proxy layer.
3.2 POST /api/rfc
Generic RFC invocation. The request body describes which function to call, what parameters to pass, and which outputs to read.
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
func_name |
string | ✅ | SAP function module name, e.g. BAPI_USER_GETLIST |
inputs |
object | ❌ | Scalar input parameters: parameter name → value (implicit: string→CHARS, integer→INT, float→FLOAT) |
table_inputs |
object | ❌ | Table input parameters: table name → array of rows; each row is field name → value |
struct_inputs |
object | ❌ | Top-level structure input: structure name → {field name → value} (e.g. ADDRESS) |
int_outputs |
string[] | ❌ | Names of integer output parameters to read (read as SAP INT) |
string_outputs |
object | ❌ | String output parameters to read: parameter name → max length. When max_len is null, the server auto-discovers it from metadata |
auto_outputs |
string[] | ❌ | Names of scalar output parameters to read by the metadata's true type (INT→integer, FLOAT→float, INT8→i64, BCD→string, BYTE/XSTRING→Base64) |
table_outputs |
object | ❌ | Output tables to read: table name → array of field objects {"name":"...","max_len":...,"auto":...} (when auto:true, read by true type; default false) |
struct_outputs |
object | ❌ | Top-level structure outputs to read: structure name → array of field objects (same field rules as table_outputs) |
read_return |
bool | ❌ | Whether to auto-read the BAPI RETURN message table; default false |
timeout_secs |
u64? | ❌ | Per-call timeout in seconds (takes effect when ≥1); if omitted/0, uses the global SAP_REQUEST_TIMEOUT_SECS (default 60s); returns 504 on timeout. Raise it for slow endpoints (batch BAPIs, large-table queries) |
Value-type rules (field values of inputs / table_inputs / struct_inputs):
- JSON string → SAP
CHARS(e.g."X","D*") - JSON integer → SAP
INT(e.g.50) - JSON float → SAP
FLOAT(e.g.123.45) - Explicit type (for BCD/INT8/binary):
{"type":"BCD","value":"999.99"},{"type":"INT8","value":9876543210},{"type":"BYTES","value":"<Base64>"}
The caller decides the type via the JSON literal or an explicit type; the server does not guess.
Response body
{
"func": "BAPI_USER_GETLIST", // echoed function name
"scalars": { // scalar outputs
"ROWS": 50, // int_outputs / auto_outputs → JSON integer
"ECHOTEXT": "Hello", // string_outputs → JSON string
"BIG_ID": 9876543210 // auto_outputs (INT8) → JSON integer
},
"tables": { // table outputs: table name → array of rows
"USERLIST": [
// auto:false (default) → field values are strings; auto:true → by true type (integer/float/Base64)
{ "USERNAME": "DEVELOPER", "ROWCOUNT": 42 }
]
},
"structs": { // top-level structure outputs (present when declared via struct_outputs; same type rules as tables)
"ADDRESS": { "FIRSTNAME": "Dev", "LASTNAME": "User" }
},
"return_table": [ // only when read_return=true and a RETURN table exists (fields are strings)
{ "TYPE": "S", "ID": "01", "NUMBER": "123", "MESSAGE": "..." }
]
}
💡 Type control for table/structure outputs: fields are read as strings by default (backwards compatible). Adding
"auto":trueto a field makes the server pick the getter by the DDIC true type (INT→integer, FLOAT→float, INT8→i64, BYTE/XSTRING→Base64, otherwise string), preserving numeric/binary semantics.
Fields you don't read (e.g. you didn't pass table_outputs) do not appear in the response (the tables object is empty).
3.3 AI-facing metadata API
15 endpoints let an AI/agent self-service discover functions, understand parameters, query the data dictionary, read docs, view source, read table data, triage short dumps, and edit code. Typical workflow: search → inspect interface → read docs → view source → call. The full operator guide for AI lives in AGENTS.md.
| Endpoint | Purpose | Example |
|---|---|---|
POST /api/functions/search |
Search functions by wildcard | {"pattern":"BAPI_USER_*","max_results":10} |
GET /api/functions/:name |
Inspect a function's full interface (parameters/types/direction/nested fields) | /api/functions/BAPI_USER_GET_DETAIL |
GET /api/functions/:name/doc |
Read docs (short text + SE37 long doc + parameter descriptions) | /api/functions/BAPI_USER_GET_DETAIL/doc?lang=EN |
GET /api/functions/:name/source |
Read a function's ABAP source (how it's implemented); ?prologue=true appends compact signatures of every CALL FUNCTION target |
/api/functions/STFC_CONNECTION/source?prologue=true |
GET /api/programs/:name/source |
Read program/report/include source | /api/programs/RSBDCOS0/source |
POST /api/table/read |
Read transparent-table data (wraps RFC_READ_TABLE) | {"table":"T000","fields":["MANDT","MTEXT"]} |
GET /api/ddic/type/:name |
Query DDIC structure/table field definitions | /api/ddic/type/BAPIRET2 |
GET /api/ddic/field/:table/:field |
Query field semantics (data element/domain/fixed values) | /api/ddic/field/BAPIRET2/TYPE |
GET /api/dumps |
Structured short-dump list (parsed from the ADT Atom feed) | /api/dumps?limit=50 |
GET /api/dumps/grouped |
Dumps grouped by (error type, terminated program) — what keeps failing | /api/dumps/grouped |
GET /api/dumps/:key/detail |
One dump's parsed detail: header, termination point, call stack | /api/dumps/<key>/detail |
PUT /api/objects/:type/:name/source |
Write full source (lock→put→unlock→activate orchestrated in one request) | {"source":"REPORT z..."} |
POST /api/objects/:type/:name/replace |
AI-style unique find-and-replace + activate | {"old_string":"...","new_string":"..."} |
POST /api/objects/:type/:name/syntax |
Syntax-check source without writing it | {"source":"REPORT z..."} |
ANY /api/adt/:path |
Generic ADT REST proxy (/sap/bc/adt/** 1:1): dumps, class sources, anything Eclipse ADT exposes; CSRF handled for write methods |
/api/adt/runtime/dumps |
End-to-end example (list users):
# 1. Search functions → 2. Inspect interface (find EXPORT table USERLIST) → 3. Call
curl http://127.0.0.1:3000/api/functions/BAPI_USER_GETLIST
curl -X POST http://127.0.0.1:3000/api/rfc -H "Content-Type: application/json" \
-d '{"func_name":"BAPI_USER_GETLIST","table_outputs":{"USERLIST":[{"name":"USERNAME","max_len":12}]},"read_return":true}'
Constraints
- DDIC type queries (endpoints 4/5) are generally available for structures; transparent tables (e.g. MARA) may return
NOT_FOUNDdepending on the target system's DDIC configuration.- Long docs (endpoint 3) rely on
DOCU_GET; on some systems where it isn't enabled,long_textis empty, but parameter descriptions still work.fixed_valuesis especially useful for understanding the legal values of status-code / enum fields.- The
/api/dumps*endpoints need ADT enabled (SAP_ADT_BASE_URL); like the ADT proxy they return 503ADT_DISABLEDotherwise. Detail parsing matches English labels — on a non-English logon, fields come back empty rather than wrong (fall back to the raw/api/adt/runtime/dump/{key}/formatted). The list/grouped endpoints parse only the structured Atom feed and cost zero detail requests.- Source endpoints (
/api/functions/:name/source,/api/programs/:name/source) read via RPY RFCs first and automatically fall back to ADT on failure (except NOT_FOUND); the response'ssource_viafield (rfc/adt) says which channel served it. Sources with lines wider than 72 chars fail the RPY path on some systems — the fallback covers that (since v0.5.1).- Write endpoints (
/api/objects/**) orchestrate the full ADT sequence (stateful session → lock → put → unlock → activate) inside one request; activation failure is a logical result (HTTP 200 +activated.problems[]), not a transport error. For function modules the parameter block in the source is metadata-owned — anchor edits in the function body. Object creation is not built yet (since v0.6.0).
4. Call Examples
4.1 Minimal connectivity test — STFC_CONNECTION
SAP's standard ping function; echoes back the text you send.
curl -X POST http://127.0.0.1:3000/api/rfc \
-H "Content-Type: application/json" \
-d '{
"func_name": "STFC_CONNECTION",
"inputs": { "REQUTEXT": "Hello from Rust!" },
"string_outputs": { "ECHOTEXT": 255, "RESPTEXT": 255 }
}'
Response:
{
"func": "STFC_CONNECTION",
"scalars": {
"ECHOTEXT": "Hello from Rust!",
"RESPTEXT": "SAP R/3 Rel. ..."
},
"tables": {}
}
4.2 Read the user list — BAPI_USER_GETLIST
curl -X POST http://127.0.0.1:3000/api/rfc \
-H "Content-Type: application/json" \
-d '{
"func_name": "BAPI_USER_GETLIST",
"inputs": { "MAX_ROWS": 50, "WITH_USERNAME": "X" },
"int_outputs": ["ROWS"],
"table_outputs": {
"USERLIST": [
{"name": "USERNAME", "max_len": 12},
{"name": "FIRSTNAME", "max_len": 40},
{"name": "LASTNAME", "max_len": 40}
]
},
"read_return": true
}'
max_lencan also be omitted — the server auto-discovers it from SAP metadata. For example,{"name": "USERNAME"}.
4.3 With a selection condition — BAPI_USER_GETLIST + SELECTION_RANGE
Demonstrates table_inputs: filter usernames starting with D.
curl -X POST http://127.0.0.1:3000/api/rfc \
-H "Content-Type: application/json" \
-d '{
"func_name": "BAPI_USER_GETLIST",
"inputs": { "MAX_ROWS": 10, "WITH_USERNAME": "X" },
"table_inputs": {
"SELECTION_RANGE": [
{
"PARAMETER": "USERNAME",
"SIGN": "I",
"OPTION": "CP",
"LOW": "D*"
}
]
},
"int_outputs": ["ROWS"],
"table_outputs": {
"USERLIST": [
{"name": "USERNAME", "max_len": 12},
{"name": "FIRSTNAME", "max_len": 40},
{"name": "LASTNAME", "max_len": 40}
]
}
}'
4.4 Calling from other languages
Python (requests)
import requests
resp = requests.post("http://127.0.0.1:3000/api/rfc", json={
"func_name": "STFC_CONNECTION",
"inputs": {"REQUTEXT": "from python"},
"string_outputs": {"ECHOTEXT": 255},
})
print(resp.json())
Node.js (fetch)
const r = await fetch("http://127.0.0.1:3000/api/rfc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
func_name: "STFC_CONNECTION",
inputs: { REQUTEXT: "from node" },
string_outputs: { ECHOTEXT: 255 },
}),
});
console.log(await r.json());
5. Common BAPI Quick Reference
The table below helps you quickly find which field names go where. For the exact available fields, consult SE37 / the official SAP docs.
| BAPI | inputs | table_inputs | Output |
|---|---|---|---|
STFC_CONNECTION |
REQUTEXT |
— | ECHOTEXT / RESPTEXT (string) |
BAPI_USER_GETLIST |
MAX_ROWS(int) / WITH_USERNAME |
SELECTION_RANGE |
ROWS(int) / USERLIST(table) |
BAPI_USER_GET_DETAIL |
USERNAME |
— | ADDRESS(struct→string) / RETURN(table) |
BAPI_MATERIAL_GETLIST |
MAXROWS(int) |
MATNRSELECTION |
MATNRLIST(table) |
Calling patterns:
- Structure input (e.g.
ADDRESS) does not currently support nested structures; only flat fields inside table rows are supported. - The
RETURNtable: most BAPIs use theBAPIRET2structure; enablingread_return: trueauto-parses the four fieldsTYPE/ID/NUMBER/MESSAGE.
6. Error Handling
6.1 HTTP status codes
The server maps HTTP status codes by error source so callers can distinguish "caller errors" (4xx) from "upstream errors" (5xx):
| Status | Trigger | Source |
|---|---|---|
200 OK |
Call succeeded; search with no match (count:0) |
RFC_OK (0) |
400 Bad Request |
Invalid request JSON; ABAP message/exception; invalid parameter/conversion failure; empty pattern | SAP 4/5/20/23; gateway PATTERN_EMPTY |
401 Unauthorized |
Missing / wrong token (when SAP_API_KEY is set) |
gateway auth layer AUTH_INVALID |
403 Forbidden |
SAP authorization check failed | SAP 25 |
404 Not Found |
Function / DDIC not found; route doesn't exist | SAP 17, or SAP 5 + key FU_NOT_FOUND/NOT_FOUND; gateway ROUTE_NOT_FOUND |
405 Method Not Allowed |
Method mismatch (e.g. POST to a GET endpoint) | gateway routing layer METHOD_NOT_ALLOWED |
422 Unprocessable Entity |
Request body missing a required field | axum deserialization JSON_INVALID |
429 Too Many Requests |
Rate limit exceeded (SAP_RATE_LIMIT_RPS) |
gateway rate-limit layer RATE_LIMITED |
500 Internal Server Error |
ABAP runtime failure, out of memory, unknown | SAP 3/11 etc. |
502 Bad Gateway |
Communication failure, connection closed by peer | SAP 1/6 |
504 Gateway Timeout |
SAP-side timeout; gateway global / per-request timeout | SAP 9; gateway timeout |
6.2 Error response body
All errors share the shape:
{
"error": {
"code": 404,
"key": "FU_NOT_FOUND",
"message": "..."
}
}
| Field | Meaning |
|---|---|
code |
HTTP status code (same as the response status line; callers use this for coarse branching). Note: not the SAP internal RC |
key |
Machine code: an SAP error key (e.g. FU_NOT_FOUND, RFC_COMMUNICATION_FAILURE) or a gateway key (AUTH_INVALID / JSON_INVALID / RATE_LIMITED / METHOD_NOT_ALLOWED / ROUTE_NOT_FOUND / PATTERN_EMPTY) |
message |
Human-readable description (may include the original SAP message text) |
💡
code= HTTP status code (the SAP internal RC is not exposed to callers). Branch coarsely oncode, finely onkey.
Special case: an empty search is not an error. POST /api/functions/search with no results returns 200 {"count":0,"functions":[]} and does not raise an error.
6.3 Troubleshooting
- Check the HTTP status code: 4xx is usually a request-parameter / ABAP business problem (recoverable by fixing the request); 5xx is an SAP system / network problem
- Check
key:RFC_COMMUNICATION_FAILUREis usually network/connection;RFC_ABAP_EXCEPTIONis an ABAP-raised error - Compare
codeagainst theRFC_RCenum in the SDK headersapnwrfc.h - Reproduce locally: run the same request body against the SAP system directly in SE37 to validate parameter names/types
7. Deployment Tips
Quick deploy: the project ships a
docker-compose.yml; once.envis set (includingSAP_SDK_HOST_PATH), rundocker compose up -d --build. See Quick Start.
7.1 sapnwrfc.dll not found
At runtime, Windows must be able to load sapnwrfc.dll. Two options:
- PATH: add
nwrfcsdk\libto the systemPATH - Same directory: copy
sapnwrfc.dllnext to the exe
If startup complains about a "DLL entry point not found" and the like, it's usually a PATH issue.
7.2 Startup failure: connection-related
Configuration load failed: missing required environment variable: SAP_ASHOST
→ .env is incomplete; see §2.
RFC call error (code: 2): ...
Can't reach SAP: verify ASHOST/SYSNR network reachability, account/password, and the CLIENT client number.
Note: the strings above mirror the actual startup/RFC messages. Depending on the build, the program currently emits these messages in Chinese; match on the error code / SAP RC rather than the exact wording.
7.3 Service deployment
- Register the binary as a boot-start service with systemd / NSSM / Windows Service
- Only listen on
0.0.0.0:3000on an internal network; for external access, add a reverse proxy (Nginx) + auth + HTTPS - Consider setting
SAP_LISTEN_ADDRto bind to the internal NIC only
7.4 Trust boundary
This service does no authentication on its own. Anyone who can reach the listen port can execute any RFC with the configured SAP account. Always place it on a controlled network or add a layer of gateway authentication.
8. Architecture and Limitations
8.1 Module structure
src/
├── main.rs Entry point: .env → start the right mode per role (client/server/both)
├── config.rs Assembles client-mode connection parameters + listen address from env vars
├── server_config.rs Server-mode config: parses servers.toml (gateway/functions/webhooks)
├── server.rs axum Router + handlers + auth/rate-limit middleware (run_blocking_with_timeout centralizes the spawn_blocking + timeout template)
├── auth.rs Optional Bearer-token auth for /api/* (SAP_API_KEY, constant-time compare); probes & doc pages always open
├── server_rfc.rs Server mode: register with the Gateway + dispatch callbacks + webhook forwarding
├── adt.rs ADT REST proxy /api/adt/**: passthrough to /sap/bc/adt/** with Basic auth + CSRF token/session handling (write methods retry once on 403)
├── dumps.rs Structured ST22 analysis /api/dumps**: Atom feed parsing, (error type × program) grouping, /formatted text → header/termination point/call stack
├── objects.rs ABAP object write orchestration /api/objects**: dedicated stateful ADT session, lock→put→unlock→activate, activation/syntax result parsing, find-and-replace editing
├── api.rs Request/response DTOs (serde) + execute_invoke execution core + input validation
├── executor.rs execute_collect: injects metadata resolution then delegates to execute_invoke
├── connection.rs RfcConnection: open/close/fetch function/pull parameter metadata (unsafe impl Send)
├── function.rs RfcFunction/RfcTable/RfcRow: parameter read/write, table ops + ScalarReader trait
├── pool.rs RfcConnectionPool: multi-connection pool + auto-reconnect + acquire timeout
├── metadata.rs Function/DDIC metadata cache (RwLock; auto-discovers field lengths and types)
├── discovery.rs AI-facing metadata wrappers (RFC_FUNCTION_SEARCH/DDIF_FIELDINFO_GET/DOCU_GET)
├── error.rs RfcError + semantic HTTP status codes mapped from SAP RC + JSON error body
├── ffi.rs Low-level C FFI bindings (sapnwrfc function signatures + RFCTYPE/direction constants)
├── string_utils.rs UTF-8 ↔ UTF-16 (SAP UC) conversion
├── index.html Home-page HTML template, English (include_str! embedded at compile time, {{BASE_URL}} placeholder)
└── index.zh.html Home-page HTML template, Chinese (selected by the client's Accept-Language: zh)
8.2 Concurrency model
[HTTP request N] ─▶ axum handler (async)
│
├─▶ run_blocking ─▶ spawn_blocking ─▶ [pool grabs an idle connection] ─▶ RfcInvoke (FFI)
│ (server.rs) (pool.rs) │
└─◀──────── await JoinHandle ◀──────────────────────────────────────────────┘
run_blocking_with_timeout(server.rs): foldsspawn_blocking + with_connection + Join error mapping + tokio::time::timeoutinto one place; shared by all RFC business handlers- Why
spawn_blocking: SAP calls are blocking FFI; running them directly on a tokio worker would stall the whole runtime - Connection pool (
pool.rs):RfcConnectionPoolmaintains a set of reusable connections — pop when idle, borrow to execute, and on communication errors (RC=1/2/3/22) drop and auto-reconnect.acquirehas a 120s total timeout cap, so it never hangs forever when the pool is exhausted - Why
unsafe impl Sendis needed:RfcConnectionholds raw pointers and is not Send; underMutexserialization (eachwith_connectionexclusively owns one connection), the NWRFC SDK permits the same connection to be used serially across threads, so it's sound - Pool size: defaults to
SAP_POOL_SIZE=8, adjustable in.env. A request grabs an idle connection from the pool and waits if none is free; on connection failure it auto-reconnects as needed - The
/api/adt/**path bypasses the pool entirely: it never touches the RFC FFI or the connection pool — it is a plain async HTTP passthrough to the ADT service (Basic auth + CSRF token/session management inadt.rs), sharing only the auth/rate-limit middleware with the RFC endpoints
8.3 Upgrade path
| Need | Status / Direction |
|---|---|
| Authentication | ✅ Implemented (optional SAP_API_KEY Bearer auth, constant-time compare, auth.rs; probes & doc pages always open) |
| Connection-pool acquire timeout | ✅ Implemented (ACQUIRE_TIMEOUT=120s; callers no longer hang forever when the pool is exhausted) |
| Read table/structure outputs by true type | ✅ Implemented (with FieldSpec.auto=true, read as INT/FLOAT/INT8/Base64) |
| Semantic HTTP error codes | ✅ Implemented (maps SAP RC to 400/403/404/500/502/504; see §6.1) |
| HTTP input validation + DoS protection | ✅ Implemented (validate_func_name format/length, max_len clamping, table_inputs row-count upper bound, table_outputs/read_return output row cap of 10,000) |
| FFI handle defense | ✅ Implemented (null checks on OpenConnection/CreateFunction/AppendNewRow return values) |
Namespaced function modules /NS/NAME |
✅ Implemented (validation + wildcard route dispatch, since v0.4.10) |
| ADT REST proxy | ✅ Implemented (/api/adt/** passthrough with automatic CSRF handling, since v0.4.11) |
| Structured ST22 analysis | ✅ Implemented (/api/dumps list/grouped/detail parsed from ADT — no more multi-hundred-KB raw texts; since v0.5.0) |
| ABAP code modification | ✅ Implemented (PUT source / POST replace / POST syntax for prog/class/func — full lock→write→activate orchestration with dedicated stateful session; since v0.6.0) |
| Source dependency prologue | ✅ Implemented (/api/functions/:name/source?prologue=true inlines compact signatures of CALL FUNCTION targets; since v0.5.0) |
| Per-IP rate limiting | ✅ Implemented (optional SAP_RATE_LIMIT_RPS, governor-keyed limiter, 429 on excess) |
| Per-RFC execution timeout | ✅ Implemented (run_blocking_with_timeout wraps spawn_blocking with tokio::time::timeout; default 60s / SAP_REQUEST_TIMEOUT_SECS, per-request timeout_secs, 504 on timeout) |
| tRFC/qRFC | Not supported yet |
9. Server Mode (Called by SAP)
Besides client mode (HTTP→SAP), this service also supports server mode: SAP calls back into this service over RFC and the call is forwarded to a configured HTTP webhook, implementing an "SAP → HTTP" reverse proxy. This is useful for letting ABAP call external microservices or for pushing business events out of SAP.
To enable: SAP_ROLE=server, together with a servers.toml configuring the gateway/program_id/functions/webhooks.
Full details (how it works, SM59 configuration, the webhook protocol, examples) are in docs/SERVER_MODE.md.
10. Releasing a New Version (Maintainers)
- Commit all changes and confirm a local build passes:
cargo test # unit tests (no SAP needed; CI runs these by default) cargo build --release # With a real SAP environment, additionally run integration tests (tests/, marked #[ignore]): # DYLD_LIBRARY_PATH=./nwrfcsdk/lib/darwin-aarch64 cargo test -- --ignored - Bump the
versionfield inCargo.toml(e.g.0.2.0→0.3.0) - Tag and push; CI automatically builds Linux/macOS/Windows binaries and uploads them to the GitHub Release:
git tag v0.3.0 git push origin v0.3.0 - Windows binaries are produced by CI automatically (a
.def+lib.exegenerates a stub import library to work around the MSVC linking restriction), so maintainers don't need to build by hand. To reproduce CI's stub linking locally, run this in an "x64 Native Tools Command Prompt":lib /def:sapnwrfc.def /machine:x64 /out:nwrfcsdk\lib\windows-x86_64\sapnwrfc.lib cargo build --release
CI workflow:
.github/workflows/release.yml. Changes to build.rs let theSAP_SDK_DIRenvironment variable point to any SDK install directory, for use with Docker / CI / custom paths.
License
This project is open-sourced under the MIT License.
About the SAP NWRFC SDK: this project links against SAP's proprietary SDK (
build.rs); its use is governed by your agreement with SAP. The MIT license applies only to the source code in this repository and does not extend to the SDK itself.
Trademarks: This is an independent, community-maintained project. It is not affiliated with, endorsed by, or sponsored by SAP SE. "SAP" and other SAP product names mentioned here are trademarks or registered trademarks of SAP SE, used solely to describe interoperability.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found