Skip to content

Commit 125c36c

Browse files
Elior Hamamyclaude
andcommitted
Propagate X-Data-Env from request onto outbound calls
createClientFromRequest extracted the auth tokens and Base44-State but no data-env signal, so a backend function's user-scoped entity calls (base44.entities.X...) always hit production data even when the app runs in test-data mode — the user JWT carries no environment, unlike the service token. Read X-Data-Env from the incoming request and add it to additionalHeaders so every outbound call (user + service role) carries it, keeping function data operations in the same environment as the triggering request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 87bba89 commit 125c36c

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

src/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export function createClientFromRequest(request: Request): Base44Client {
397397
const serverUrlHeader = request.headers.get("Base44-Api-Url");
398398
const functionsVersion = request.headers.get("Base44-Functions-Version");
399399
const stateHeader = request.headers.get("Base44-State");
400+
const dataEnvHeader = request.headers.get("X-Data-Env");
400401

401402
if (!appId) {
402403
throw new Error(
@@ -439,6 +440,14 @@ export function createClientFromRequest(request: Request): Base44Client {
439440
if (stateHeader) {
440441
additionalHeaders["Base44-State"] = stateHeader;
441442
}
443+
// Propagate the data environment so entity operations from the function stay
444+
// in the same environment (e.g. test data) as the triggering request. This
445+
// matters for the user-scoped client: unlike the service token, the user JWT
446+
// carries no data-env, so without forwarding this header the callbacks fall
447+
// back to production data even when the app runs in test-data mode.
448+
if (dataEnvHeader) {
449+
additionalHeaders["X-Data-Env"] = dataEnvHeader;
450+
}
442451

443452
return createClient({
444453
serverUrl: serverUrlHeader || "https://base44.app",

tests/unit/client.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,61 @@ describe('Service Role Authorization Headers', () => {
540540
expect(scope.isDone()).toBe(true);
541541
});
542542

543+
test('should propagate X-Data-Env header on user-scoped API requests when created from request', async () => {
544+
const mockRequest = {
545+
headers: {
546+
get: (name) => {
547+
const headers = {
548+
'Authorization': 'Bearer user-token-123',
549+
'Base44-App-Id': appId,
550+
'Base44-Api-Url': serverUrl,
551+
'X-Data-Env': 'dev'
552+
};
553+
return headers[name] || null;
554+
}
555+
}
556+
};
557+
558+
const client = createClientFromRequest(mockRequest);
559+
560+
// The user-scoped client (not asServiceRole) must still carry the data env
561+
// so test-mode function callbacks hit test data, not production.
562+
scope.get(`/api/apps/${appId}/entities/Todo`)
563+
.matchHeader('X-Data-Env', 'dev')
564+
.matchHeader('Authorization', 'Bearer user-token-123')
565+
.reply(200, { items: [], total: 0 });
566+
567+
await client.entities.Todo.list();
568+
569+
expect(scope.isDone()).toBe(true);
570+
});
571+
572+
test('should not include X-Data-Env header when not present in original request', async () => {
573+
const mockRequest = {
574+
headers: {
575+
get: (name) => {
576+
const headers = {
577+
'Authorization': 'Bearer user-token-123',
578+
'Base44-App-Id': appId,
579+
'Base44-Api-Url': serverUrl
580+
};
581+
return headers[name] || null;
582+
}
583+
}
584+
};
585+
586+
const client = createClientFromRequest(mockRequest);
587+
588+
scope.get(`/api/apps/${appId}/entities/Todo`)
589+
.matchHeader('X-Data-Env', (val) => !val) // Should not have this header
590+
.matchHeader('Authorization', 'Bearer user-token-123')
591+
.reply(200, { items: [], total: 0 });
592+
593+
await client.entities.Todo.list();
594+
595+
expect(scope.isDone()).toBe(true);
596+
});
597+
543598
test('should not include Base44-State header when not present in original request', async () => {
544599
const mockRequest = {
545600
headers: {

0 commit comments

Comments
 (0)