Skip to content

Commit 4aafd76

Browse files
akoclaude
andcommitted
fix: REST client BASIC auth uses Rest\$ConstantValue with correct BSON key (#200)
Three root causes found and fixed: 1. Studio Pro requires Rest\$ConstantValue for BASIC auth credentials, not Rest\$StringValue. Using StringValue causes InvalidCastException when opening the document and 401 at runtime because the auth header is never sent. 2. The BSON field name for the constant reference is "Value" (matching the metamodel), not "Constant" (which was an incorrect guess). With the wrong key, Mendix can't resolve the constant → CE7073. 3. When literal strings are provided for auth credentials (e.g., Password: 'secret'), the executor now auto-creates string constants (Module.ClientName_Username / _Password) so the BSON always contains Rest\$ConstantValue references. This is transparent to the user. Also adds missing ExportLevel, Tags, Timeout, BaseUrlParameter, and OpenApiFile fields that Studio Pro always populates on REST clients. Test case: mdl-examples/bug-tests/200-basic-auth-rest-client.mdl creates a REST client pointing at httpbin.org/basic-auth with a test page for side-by-side comparison of REST client auth vs inline REST CALL auth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0e28aba commit 4aafd76

File tree

5 files changed

+257
-13
lines changed

5 files changed

+257
-13
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
-- ============================================================================
2+
-- Bug #200: REST Client BASIC Auth — runtime verification test case
3+
-- ============================================================================
4+
--
5+
-- This script creates a REST client with BASIC authentication and a microflow
6+
-- that calls it. Open the project in Studio Pro and run the microflow to
7+
-- verify the Authorization header is sent correctly.
8+
--
9+
-- The test uses httpbin.org/basic-auth which returns 200 only if the correct
10+
-- Basic auth credentials are provided, and 401 otherwise.
11+
--
12+
-- Usage:
13+
-- mxcli exec mdl-examples/bug-tests/200-basic-auth-rest-client.mdl -p app.mpr
14+
-- Then open in Studio Pro and run ACT_TestBasicAuth from the test page.
15+
--
16+
-- Expected result:
17+
-- - ACT_TestBasicAuth logs "Auth OK: 200" (httpbin returns the auth echo)
18+
-- - If auth header is NOT sent, httpbin returns 401 and the log shows "Auth FAILED"
19+
--
20+
-- ============================================================================
21+
22+
CREATE MODULE AuthTest;
23+
24+
-- ############################################################################
25+
-- PART 1: CONSTANTS FOR AUTH CREDENTIALS
26+
-- ############################################################################
27+
28+
-- Mendix requires Rest$ConstantValue for BASIC auth (not Rest$StringValue).
29+
-- Literal strings in Authentication: BASIC (...) are auto-promoted to
30+
-- constants by mxcli. For explicit control, create constants first:
31+
32+
CREATE CONSTANT AuthTest.ApiUser TYPE String DEFAULT 'testuser';
33+
CREATE CONSTANT AuthTest.ApiPass TYPE String DEFAULT 'testpass123!';
34+
35+
-- ############################################################################
36+
-- PART 2: REST CLIENT WITH BASIC AUTH
37+
-- ############################################################################
38+
39+
-- httpbin.org/basic-auth/{user}/{pass} returns 200 + JSON if correct
40+
-- Basic auth credentials are sent, 401 otherwise.
41+
-- This is the definitive test: if we get 200, the header was sent.
42+
43+
CREATE REST CLIENT AuthTest.HttpBinAuthAPI (
44+
BaseUrl: 'https://httpbin.org',
45+
Authentication: BASIC (Username: $ApiUser, Password: $ApiPass)
46+
)
47+
{
48+
/** Calls /basic-auth/testuser/testpass123! — returns 200 if auth header sent */
49+
OPERATION CheckAuth {
50+
Method: GET,
51+
Path: '/basic-auth/testuser/testpass123!',
52+
Headers: ('Accept' = 'application/json'),
53+
Timeout: 30,
54+
Response: NONE
55+
}
56+
};
57+
58+
-- ############################################################################
59+
-- PART 2: TEST MICROFLOW
60+
-- ############################################################################
61+
62+
/**
63+
* Test BASIC auth on consumed REST client.
64+
*
65+
* Calls httpbin.org/basic-auth/testuser/testpass123! which requires
66+
* correct Basic auth. Logs the HTTP status code:
67+
* 200 = auth header sent correctly
68+
* 401 = auth header missing or wrong (BUG #200)
69+
*/
70+
CREATE MICROFLOW AuthTest.ACT_TestBasicAuth ()
71+
RETURNS Boolean AS $Success
72+
BEGIN
73+
DECLARE $Success Boolean = false;
74+
75+
-- Call the REST client operation
76+
SEND REST REQUEST AuthTest.HttpBinAuthAPI.CheckAuth;
77+
78+
-- Check the response status
79+
IF $latestHttpResponse/StatusCode = 200 THEN
80+
LOG INFO NODE 'AuthTest' 'Auth OK: 200 — Basic auth header was sent correctly';
81+
SET $Success = true;
82+
ELSE
83+
LOG WARNING NODE 'AuthTest' 'Auth FAILED: status=' + toString($latestHttpResponse/StatusCode) + ' — Basic auth header was NOT sent (bug #200)';
84+
END IF;
85+
86+
RETURN $Success;
87+
END;
88+
/
89+
90+
-- ############################################################################
91+
-- PART 3: COMPARISON — INLINE REST CALL (known to work)
92+
-- ############################################################################
93+
94+
/**
95+
* Same test using inline REST CALL (this always worked).
96+
* Run both microflows and compare: if ACT_TestBasicAuth fails but
97+
* ACT_TestBasicAuthInline succeeds, the bug is in the REST client
98+
* auth serialization.
99+
*/
100+
CREATE MICROFLOW AuthTest.ACT_TestBasicAuthInline ()
101+
RETURNS Boolean AS $Success
102+
BEGIN
103+
DECLARE $Success Boolean = false;
104+
105+
$Response = REST CALL GET 'https://httpbin.org/basic-auth/testuser/testpass123!'
106+
HEADER Accept = 'application/json'
107+
AUTH BASIC 'testuser' PASSWORD 'testpass123!'
108+
TIMEOUT 30
109+
RETURNS String
110+
ON ERROR CONTINUE;
111+
112+
IF $Response != '' THEN
113+
LOG INFO NODE 'AuthTest' 'Inline auth OK — response received';
114+
SET $Success = true;
115+
ELSE
116+
LOG WARNING NODE 'AuthTest' 'Inline auth FAILED — empty response';
117+
END IF;
118+
119+
RETURN $Success;
120+
END;
121+
/
122+
123+
-- ############################################################################
124+
-- PART 4: TEST PAGE
125+
-- ############################################################################
126+
127+
CREATE PAGE AuthTest.AuthTestPage
128+
(
129+
Title: 'Bug #200 — Basic Auth Test',
130+
Layout: Atlas_Core.Atlas_Default,
131+
Url: 'auth-test'
132+
)
133+
{
134+
LAYOUTGRID grid {
135+
ROW rowTitle {
136+
COLUMN col (DesktopWidth: 12) {
137+
DYNAMICTEXT txtTitle (Content: 'Bug #200: REST Client Basic Auth', RenderMode: H2)
138+
DYNAMICTEXT txtDesc (Content: 'Click each button and check the console log. Both should show status 200.', RenderMode: Paragraph)
139+
}
140+
}
141+
ROW rowButtons {
142+
COLUMN colClient (DesktopWidth: 6) {
143+
ACTIONBUTTON btnClient (
144+
Caption: 'Test REST Client Auth',
145+
Action: MICROFLOW AuthTest.ACT_TestBasicAuth,
146+
ButtonStyle: Primary
147+
)
148+
DYNAMICTEXT txtClient (Content: 'Uses CREATE REST CLIENT with BASIC auth', RenderMode: Paragraph)
149+
}
150+
COLUMN colInline (DesktopWidth: 6) {
151+
ACTIONBUTTON btnInline (
152+
Caption: 'Test Inline REST CALL Auth',
153+
Action: MICROFLOW AuthTest.ACT_TestBasicAuthInline,
154+
ButtonStyle: Default
155+
)
156+
DYNAMICTEXT txtInline (Content: 'Uses inline REST CALL ... AUTH BASIC (known working)', RenderMode: Paragraph)
157+
}
158+
}
159+
}
160+
};
161+
162+
-- ############################################################################
163+
-- PART 5: VERIFICATION
164+
-- ############################################################################
165+
166+
SHOW REST CLIENTS IN AuthTest;
167+
DESCRIBE REST CLIENT AuthTest.HttpBinAuthAPI;
168+
DESCRIBE MICROFLOW AuthTest.ACT_TestBasicAuth;

mdl/executor/cmd_rest_clients.go

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,44 @@ func createRestClient(ctx *ExecContext, stmt *ast.CreateRestClientStmt) error {
337337
BaseUrl: stmt.BaseUrl,
338338
}
339339

340-
// Authentication
340+
// Authentication — Mendix requires Rest$ConstantValue for BASIC auth credentials
341+
// (Rest$StringValue causes InvalidCastException in Studio Pro). When literal
342+
// strings are provided, auto-create constants to hold them.
341343
if stmt.Authentication != nil {
342-
svc.Authentication = &model.RestAuthentication{
343-
Scheme: stmt.Authentication.Scheme,
344-
Username: stmt.Authentication.Username,
345-
Password: stmt.Authentication.Password,
344+
auth := &model.RestAuthentication{
345+
Scheme: stmt.Authentication.Scheme,
346346
}
347+
// Username — must use Rest$ConstantValue pointing to a real constant.
348+
// $Variable refs pass through; literal strings auto-create constants.
349+
if strings.HasPrefix(stmt.Authentication.Username, "$") {
350+
// Already a $Constant ref — resolve to qualified name for BY_NAME lookup
351+
name := strings.TrimPrefix(stmt.Authentication.Username, "$")
352+
if !strings.Contains(name, ".") {
353+
name = moduleName + "." + name
354+
}
355+
auth.Username = "$" + name
356+
} else if stmt.Authentication.Username != "" {
357+
constName := stmt.Name.Name + "_Username"
358+
if err := e.ensureConstant(moduleName, containerID, constName, stmt.Authentication.Username); err != nil {
359+
return fmt.Errorf("failed to create username constant: %w", err)
360+
}
361+
auth.Username = "$" + moduleName + "." + constName
362+
}
363+
// Password
364+
if strings.HasPrefix(stmt.Authentication.Password, "$") {
365+
name := strings.TrimPrefix(stmt.Authentication.Password, "$")
366+
if !strings.Contains(name, ".") {
367+
name = moduleName + "." + name
368+
}
369+
auth.Password = "$" + name
370+
} else if stmt.Authentication.Password != "" {
371+
constName := stmt.Name.Name + "_Password"
372+
if err := e.ensureConstant(moduleName, containerID, constName, stmt.Authentication.Password); err != nil {
373+
return fmt.Errorf("failed to create password constant: %w", err)
374+
}
375+
auth.Password = "$" + moduleName + "." + constName
376+
}
377+
svc.Authentication = auth
347378
}
348379

349380
// Operations
@@ -457,6 +488,30 @@ func convertMappingEntries(entries []ast.RestMappingEntry, importDirection bool)
457488
return result
458489
}
459490

491+
// ensureConstant creates a string constant if it doesn't already exist.
492+
func (e *Executor) ensureConstant(moduleName string, containerID model.ID, constName, value string) error {
493+
// Check if constant already exists
494+
constants, _ := e.reader.ListConstants()
495+
h, _ := e.getHierarchy()
496+
for _, c := range constants {
497+
modID := h.FindModuleID(c.ContainerID)
498+
modName := h.GetModuleName(modID)
499+
if modName == moduleName && c.Name == constName {
500+
return nil // already exists
501+
}
502+
}
503+
504+
// Create the constant
505+
constant := &model.Constant{
506+
ContainerID: containerID,
507+
Name: constName,
508+
Type: model.ConstantDataType{Kind: "String"},
509+
DefaultValue: value,
510+
ExportLevel: "Hidden",
511+
}
512+
return e.writer.CreateConstant(constant)
513+
}
514+
460515
// dropRestClient handles DROP REST CLIENT statement.
461516
func dropRestClient(ctx *ExecContext, stmt *ast.DropRestClientStmt) error {
462517

sdk/mpr/parser_rest.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,15 @@ func extractRestValue(v any) string {
334334
case "Rest$StringValue":
335335
return extractString(valMap["Value"])
336336
case "Rest$ConstantValue":
337-
return extractString(valMap["Constant"])
337+
// The BSON field is "Value" (QualifiedName of the constant).
338+
// Historical code wrote "Constant" — try both for backward compat.
339+
if v := extractString(valMap["Value"]); v != "" {
340+
return "$" + v
341+
}
342+
if v := extractString(valMap["Constant"]); v != "" {
343+
return "$" + v
344+
}
345+
return ""
338346
}
339347
return ""
340348
}

sdk/mpr/writer_rest.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ func (w *Writer) serializeConsumedRestService(svc *model.ConsumedRestService) ([
4949
"Name": svc.Name,
5050
"Documentation": svc.Documentation,
5151
"Excluded": svc.Excluded,
52+
// ExportLevel: whether the document is exposed to other modules/projects.
53+
// Studio Pro defaults to "Hidden". Missing this field has been observed
54+
// to cause runtime auth issues (#200).
55+
"ExportLevel": "Hidden",
56+
"BaseUrlParameter": nil,
57+
"OpenApiFile": nil,
5258
}
5359

5460
// BaseUrl as Rest$ValueTemplate
@@ -97,11 +103,12 @@ func serializeRestAuthScheme(auth *model.RestAuthentication) bson.M {
97103
// Values starting with "$" are treated as constant references; others as string literals.
98104
func serializeRestValue(value string) bson.M {
99105
if strings.HasPrefix(value, "$") {
100-
// Constant reference — strip the $ prefix for the constant name
106+
// Constant reference — the BSON field is "Value" (QualifiedName of the constant).
107+
constRef := strings.TrimPrefix(value, "$")
101108
return bson.M{
102-
"$ID": idToBsonBinary(generateUUID()),
103-
"$Type": "Rest$ConstantValue",
104-
"Constant": strings.TrimPrefix(value, "$"),
109+
"$ID": idToBsonBinary(generateUUID()),
110+
"$Type": "Rest$ConstantValue",
111+
"Value": constRef,
105112
}
106113
}
107114
return bson.M{
@@ -119,9 +126,15 @@ func serializeRestOperation(op *model.RestClientOperation) bson.M {
119126
"Name": op.Name,
120127
}
121128

122-
if op.Timeout > 0 {
123-
doc["Timeout"] = int64(op.Timeout)
129+
// Timeout: Studio Pro always writes this field; default is 300 seconds.
130+
timeout := int64(op.Timeout)
131+
if timeout <= 0 {
132+
timeout = 300
124133
}
134+
doc["Timeout"] = timeout
135+
136+
// Tags: Studio Pro always writes this field (versioned string array).
137+
doc["Tags"] = bson.A{int32(1)}
125138

126139
// Method: polymorphic (WithBody or WithoutBody)
127140
doc["Method"] = serializeRestMethod(op)

sdk/mpr/writer_rest_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func TestSerializeConsumedRestServiceWithConstantAuth(t *testing.T) {
135135
t.Fatalf("Username: expected map, got %T", authScheme["Username"])
136136
}
137137
assertField(t, username, "$Type", "Rest$ConstantValue")
138-
assertField(t, username, "Constant", "MyModule.ApiUser")
138+
assertField(t, username, "Value", "MyModule.ApiUser")
139139
}
140140

141141
func TestSerializeRestOperationGetWithParams(t *testing.T) {

0 commit comments

Comments
 (0)